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 # Changelog
All notable changes to MetonaSqlark will be documented in this file. All notable changes to MetonaSqlark will be documented in this file.
## [0.2.5] - 2026-07-29 ## [0.3.2] - 2026-08-08
### Fixed ### Added
- **版本号统一** — `constants.ts` VERSION 从 `'0.2.0'` 更新为 `'0.2.5'`,修正 `index.ts`/`utils.ts`/`CONTRIBUTING.md` 中过时的版本注释和数据 - **CASE WHEN 用于 WHERE** — `WHERE CASE WHEN cond THEN a ELSE b END = 'x'` 逐行求值(含 NOT/AND/OR 组合)
- **AriaEngine OPFS 后端映射** — `core.ts``mode: 'aria'` + `diskEngine: 'opfs'` 时实际使用 Memory 后端的 bug 已修复 - **CASE WHEN 用于聚合** — `SUM(CASE WHEN age > 18 THEN 1 ELSE 0 END)``COUNT/AVG/MIN/MAX(CASE...)``GROUP BY + CASE` 非聚合列
- **`_onError` 接入执行路径** — `query()`/`defineTable()`/`dropTable()`/`transaction()`/`importTable()` 的 catch 路径现在调用 `_onError` 全局错误回调 - **JOIN 哈希连接** — 等值 ON + 右表主键/索引列时,收集左表连接值 → 一次 `$in` 查询 → 哈希映射匹配,替代嵌套循环(INNER/LEFT JOINO(N+M));不适用自动回退
- **`maxRowsPerQuery` 生效** — `QueryExecutor` 构造时接收 `maxRowsPerQuery` 参数,SELECT 结果在返回前截断 - **多标签页同步** — `multiTabSync: true` 配置 + BroadcastChannel
- **WAL full 模式真正同步** — `WAL.append()` 改为 `async``full` 模式下 `await this.store.append()` 真正等待写入完成,不再 fire-and-forget - SQL 写语句(INSERT/UPDATE/DELETE/DDL)自动广播表变更
- **PluginManager.install 传 db 实例** — `register()` 增加可选 `db` 参数,`core.ts` 初始化时传入 `this`,插件可获取 db 引用 - Table API 写入(insert/insertMany/update/delete/clear/drop)自动广播
- 其他标签页收到 `external` 变更事件(订阅者可见)+ Hybrid 内存自动重载
### Changed - `db.broadcastChange(table)` 手动广播
- **SSTableReader 二分查找统一** — `locateBlockGE`/`locateBlockLE` 从线性扫描改为二分查找,rangeScan 性能在大型 SSTable 下不再退化 - **IndexedDB schema 持久化** — `__metona_schema` store 保存完整列定义,reopen(页面刷新/重连)后恢复;旧数据回退为"主键 + 索引 + 样例推断"
- **crypto 实例化** — 全局状态改为 `CryptoManager` 类,每个 AriaEngine 实例可拥有独立加密配置,保留全局函数向后兼容
- **compactLevelSync 接口公开化** — LSM 新增 public `compactLevel()` 方法,`vacuum()` 不再使用 `as any` 绕过 private 访问 ### Fixed
- **WAL 大小阈值接入 checkpoint** — `CheckpointManager` 接收 `walSizeThreshold` 参数,WAL 缓冲超阈值时自动触发 checkpoint - **IndexedDB reopen 后 schema 丢失(严重)** — schema 仅存内存缓存,close 后重开连接表结构即丢失(INSERT 列映射截断、UPDATE 全列失效)。v0.3.2 持久化 + 重建
- **where-matcher 顶层 $not 不生效** — `NOT (expr)` 解析为顶层 `{ $not }` 但被当作字段匹配,始终返回 true
### Added - **CASE 剥离后空 `$not` 恒假** — 引擎层执行 `NOT(true)` 过滤掉所有行
- **ALTER TABLE 语法** — 支持 `ALTER TABLE ... ADD COLUMN` / `DROP COLUMN`(含可选 COLUMN 关键字)
- **TRUNCATE TABLE 语法** — 支持 `TRUNCATE TABLE name` 快速清空表数据 ### Changed
- **MVCC 接入读写路径** — 事务内 insert/update/delete 调用 `mvcc.writeVersion`/`mvcc.deleteVersion`,版本链作为 undo log - 版本号升至 v0.3.2
- **IndexedDB 索引利用** — `IndexedDBEngine.find` 等值查询时优先使用 IDB 索引(`idx_col` 命名约定),避免全量 getAll
- **SQL 注入防护** — React `useTable` / Vue `useSqlarkTable` 增加表名合法性校验(`/^[a-zA-Z_][a-zA-Z0-9_]*$/` ---
--- ## [0.3.1] - 2026-08-08
## [0.2.4] - 2026-07-27 ### Added
- **CASE WHEN 表达式(SELECT 列)** — `CASE WHEN cond THEN value [WHEN ...] [ELSE value] END [AS alias]`
### Added - 多 WHEN 按顺序匹配、ELSE 缺省返回 null
- **二级索引** — 每列可独立维护 LSM Tree 索引,`$eq`/`$in`/`$gt`/`$lt` 走索引 O(log n) - THEN/ELSE 值支持字面量(字符串/数字/布尔/null)与列引用
- **MVCC 接入引擎** — MVCCManager 正式投产,替换 ad-hoc txnSnapshot - 条件支持 AND/OR/比较运算符/IS NULL 等完整 WHERE 语法
- **MVCC 自动 GC** — 每 10 次 checkpoint 自动回收过旧版本(保留最新 100 个) - 引擎层返回原始行供求值(避免投影丢失条件列),与普通列混合投影
- **Bloom Filter 序列化** — 写入 SSTable footer + 读取时加载 + 查询时 probe 快速否定 - **JOIN + 关联子查询** — JOIN 结果的 WHERE 支持 `EXISTS (SELECT ... WHERE o2.x = u.y)``$col` 引用外层行,逐行绑定求值
- **WAL 大小阈值** — `walSizeThreshold` 配置(默认 16MB),超阈值强制 checkpoint - **WAL 批量组提交** — `WAL.appendBatch()` 合并多条记录为一次底层写入;引擎 insert/update/delete 批量记录后一次落盘(写放大从 O(N) 降为 O(1))
- **内存预算** — `maxMemoryMB` 配置(默认 64MB - **`parseWhereCondition()` 导出** — 独立条件表达式解析(CASE WHEN 求值内部使用
### Changed ### Fixed
- **WAL 默认同步模式** — `walSyncMode``'batch'` 改为 `'full'`,消除 crash 丢数据风险 - **LSM pending frozen 可见性** — 异步 flush 链期间,中间冻结的 MemTable 仅存在于闭包中,读取路径不可见导致数据缺失。新增 `frozenMemtables` 列表,get/rangeScan/getAllEntries 遍历所有 pending frozen(新→旧)
- **查询优化** — `tryIndexLookup` 扩展支持非 PK 列索引查找 - **flush 缓存裁剪与预加载竞争** — flush 完成时的 LRU 裁剪会驱逐 prefetch 刚加载的 SSTable,导致扫描静默丢数据。prefetchRange/prefetchKeys 先等待 flush 链完成再加载
- **CASE 值解析顺序** — `true`/`false`/`null` 字面量被误判为列引用
---
### Changed
## [0.2.3] - 2026-07-27 - 版本号升至 v0.3.1
### Fixed ---
- **RB-Tree fixDelete 完整实现** — 补全标准红黑树删除修复,保证 O(log n)
- **LSM SSTable 缓存预热** — `init()` 预加载所有 SSTable,消除 cache miss ## [0.3.0] - 2026-08-08
- **OPFS 数据恢复** — `open()` 自动从 OPFS 文件加载已有表数据到内存
- **Aria WAL 事务恢复** — 两阶段恢复:仅回放已提交事务,未提交数据不回放 ### Added
- **LZ4 格式修复** — 重写 token 格式,消除中间字面量 token 歧义 - **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`),右侧结果按位置投影到左侧列结构
## [0.2.2] - 2026-07-27 - **CREATE INDEX / DROP INDEX** — 动态二级索引:
- `CREATE [UNIQUE] INDEX idx ON table (col)` / `DROP INDEX idx ON table (col)`
### Added - 五引擎实现:Memory 哈希索引 / IndexedDB 版本升级建 `idx_col` / OPFS 委托 / Hybrid 双写 / Aria 动态 LSM 索引
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖 - Aria 主键索引受保护(不可 DROP);CREATE INDEX 对已有数据立即建索引(回填),后续写入自动同步
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()` - **EXISTS / NOT EXISTS** — 支持关联子查询(`WHERE EXISTS (SELECT 1 FROM o WHERE o.user_id = u.id)`),逐行绑定外层上下文求值
- 支持 read/write/delete/list/exists/clear 完整接口 - **别名 WHERE 修复** — `WHERE u.age > 20`(主表别名前缀)此前不工作,v0.3.0 字段名规范化(JOIN 路径不受影响)
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件 - **`SELECT 1` 常量列** — 数字字面量可作为 SELECT 列(EXISTS 子查询常见用法)
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ... - **benchmark 页面** — `site/benchmark.html` 浏览器内实测 Memory / Aria 引擎 × 1K/10K/50K 行的 INSERT/PK 查询/索引查询/UPDATE/DELETE 吞吐(ops/sec
- **`parseAll` / 新 AST 类型导出** — `SelectUnionStatement` / `CreateIndexStatement` / `DropIndexStatement` / `BeginTransactionStatement`
### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend ### Changed
- 版本号升至 v0.3.0
---
### Fixed
## [0.2.1] - 2026-07-27 - `NOT EXISTS` 解析丢 EXISTS token 导致 parse 失败
### Fixed ---
- **WAL CRC 校验验证** — 恢复时计算并验证 CRC32,损坏记录自动跳过并告警
- **Hybrid 引擎提交顺序** — commit 先写磁盘再写内存,磁盘失败回滚内存,消除数据不一致风险 ## [0.2.6] - 2026-08-08
- **RESTRICT 外键行为修正** — 检测到引用行时抛出 `FOREIGN_KEY_VIOLATION`,不再静默保留孤儿
- **SSTable rangeScan 边界保护** — 空索引或 startBlockIdx > endBlockIdx 时安全返回 ### Fixed
- **AriaEngine 二级索引 SSTable 互相覆盖(严重)** — 主 LSM 与二级索引 LSM 此前共享同一 `sstableStore`id 空间 + meta 列表),`allocateId()` 返回 `Date.now()` 且各 LSM 从同一 meta 恢复 `nextSSTableId`,高频写入时 id 冲突导致索引数据文件覆盖主数据、meta 与数据错配。v0.2.6 按命名空间隔离(文件前缀 + meta key + 独立 id 序列),索引 LSM 各持独立 store
### Added - **LSM 多版本读取顺序错误(严重)** — `levels[]` 数组按 `push` 追加(新文件在尾部),但 `get()`/`rangeScan` 从头遍历(旧文件优先),同一 key 跨多次 flush 更新后读到旧值;`MergeIterator` 去重只取"先出堆者",不保证最新来源。v0.2.6 改为"数组头部即最新"(`unshift` + 按 id 降序加载),MergeIterator 显式取 sourceIndex 最小者
- **ColumnDef 约束激活** — `maxLength`/`min`/`max` 约束在 `checkFieldType` 中正式生效 - **LZ4 往返不一致** — ① 匹配长度超过 19 字节被截断但 `si` 跳过全部匹配,解压丢数据;② matchField=0 的组合 token 与纯字面量 token 格式歧义;③ 字面量累积超过 15 字节后组合 token 永远无法输出;④ 输出缓冲区 `maxOut` 不足导致越界写入;⑤ 压缩失败时返回原样 input,解压端无法区分。v0.2.6 重写压缩器(长匹配分段、纯字面量 token 语义明确、动态上限)
- **查询结果上限** — `DatabaseConfig.maxRowsPerQuery`(默认 0 不限制),防止超大结果集 OOM - **React/Vue 集成运行时 ReferenceError** — `useDatabase`/`useSqlarkDatabase``import type` 导入 `MetonaSqlark` 但运行时 `new MetonaSqlark()``@ts-nocheck` 掩盖此 bug,实际使用必炸。改为 value import
- **onError 全局回调** — 配置中的 `onError` 回调实际接CRUD 异常路径 - **sstableCache 无限增长** — SSTable 缓存无内存上限且 `init()` 预加载全部数据,大库 OOM。v0.2.6 引LRU 容量上限(`bufferPoolPages` × `pageSize`),查询前异步预加载兜底(修复缓存未命中静默返回 null 丢数据的隐患)
- **debug 调试模式** — `DatabaseConfig.debug: true` 输出 `[MetonaSqlark:name]` 前缀的结构化日志 - **integrations 子路径缺失** — README 宣称 `metona-sqlark/react``metona-sqlark/vue` 可用但 `exports` 未声明,现已补齐(react/vue 为 peer dependencyrollup external
- **浏览器兼容性声明** — README 增加 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持矩阵
### Changed
### Changed - **Buffer Pool 配置真正生效** — `bufferPoolPages` 从无效配置变为 SSTable 缓存容量上限(默认 256 页 ≈ 1MB 可控内存)
- MemoryEngine RESTRICT 外键行为:从"不级联保留孤儿"改为"禁止删除抛异常" - **LZ4 格式变更** — 旧版本压缩数据与新解压器不兼容,使用 `compression: true` 的用户需重新导入数据
- Hybrid 引擎 commitTransaction 磁盘优先于内存 - **SSTable 持久化串行化** — flush/compaction 改为串行链(`flushChain`),id 分配与持久化顺序一致,消除 fire-and-forget 竞态
- DB_DEFAULTS 新增 `maxRowsPerQuery: 0``debug: false` - 文档数据修正 — 测试 761(41 套件)、覆盖率 81.1%(此前 91.0% 为排除 Aria 模块的陈旧数据)、体积 ~105KB / gzip ~27KB
--- ### Added
- **AriaEngine 缓存内存上限测试** — 9 个测试:LRU 上限约束、驱逐后全表/PK/索引查询完整性、UPDATE/DELETE 全量作用、写路径上限、命名空间隔离回归、多版本回归、tombstone 回归
## [0.2.0] - 2026-07-27 - **LZ4 往返一致性测试** — 6 个往返测试(重复/文本/随机/边界长度/15 字节边界/长匹配)
- **CryptoManager 加解密测试** — 7 个测试(往返/错误密码/不同 salt/实例隔离/全局兼容层/4KB 页面)
### Added - **React/Vue hooks 集成测试** — 16 个测试(jest.mock 零依赖:mount 执行、错误路径、refresh、注入防护、数据库初始化)
- **AriaEngine 自研存储引擎** — 基于 LSM-Tree 的页面式存储引擎,19 个新模块,~3500 行 TypeScript - **jest.setup.js WebCrypto polyfill** — jsdom 环境补齐 `crypto.subtle` 支持 AES-GCM
- **页面格式层** (`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 双哈希,快速键否定判定 ## [0.2.5] - 2026-07-29
- **SSTable 构建器/读取器** (`index/sstable_builder.ts`, `sstable.ts`): 二分查找 + 范围扫描
- **Merge Iterator** (`index/merge_iterator.ts`): 最小堆多路归并,去重保留最新值 ### Fixed
- **WAL** (`wal/`): 二进制日志格式 (LSN/type/txnId/table/key/json/CRC) + Checkpoint 管理 - **版本号统一** — `constants.ts` VERSION 从 `'0.2.0'` 更新为 `'0.2.5'`,修正 `index.ts`/`utils.ts`/`CONTRIBUTING.md` 中过时的版本注释和数据
- **MVCC** (`transaction/mvcc.ts`): 版本链 + 快照隔离 + GC - **AriaEngine OPFS 后端映射** — `core.ts``mode: 'aria'` + `diskEngine: 'opfs'` 时实际使用 Memory 后端的 bug 已修复
- **存储后端** (`store/`): IndexedDB / Memory 双后端抽象 - **`_onError` 接入执行路径** — `query()`/`defineTable()`/`dropTable()`/`transaction()`/`importTable()` 的 catch 路径现在调用 `_onError` 全局错误回调
- **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩 - **`maxRowsPerQuery` 生效** — `QueryExecutor` 构造时接收 `maxRowsPerQuery` 参数,SELECT 结果在返回前截断
- **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活 - **WAL full 模式真正同步** — `WAL.append()` 改为 `async``full` 模式下 `await this.store.append()` 真正等待写入完成,不再 fire-and-forget
- **Schema 持久化** — 表结构自动保存到 `__aria_schemas`,重启自动恢复 - **PluginManager.install 传 db 实例** — `register()` 增加可选 `db` 参数,`core.ts` 初始化时传入 `this`,插件可获取 db 引用
- **SSTable 元数据管理** — SSTable 索引信息持久化,启动时自动扫描加载
- **事务感知 CRUD** — insert/update/delete 在事务中缓冲到 snapshotcommit 批量写入 LSM ### Changed
- **244 个 AriaEngine 专项测试** — 覆盖生命周期/表管理/CRUD/事务/持久化/SQL 集成/页面格式/LSM/压缩 - **SSTableReader 二分查找统一** — `locateBlockGE`/`locateBlockLE` 从线性扫描改为二分查找,rangeScan 性能在大型 SSTable 下不再退化
- **crypto 实例化** — 全局状态改为 `CryptoManager` 类,每个 AriaEngine 实例可拥有独立加密配置,保留全局函数向后兼容
### Changed - **compactLevelSync 接口公开化** — LSM 新增 public `compactLevel()` 方法,`vacuum()` 不再使用 `as any` 绕过 private 访问
- `StorageMode` 类型新增 `'aria'` - **WAL 大小阈值接入 checkpoint** — `CheckpointManager` 接收 `walSizeThreshold` 参数,WAL 缓冲超阈值时自动触发 checkpoint
- `STORAGE_MODES` 数组新增 `'aria'`
- `createEngine()` 支持 `mode: 'aria'` 分支 ### Added
- 测试从 318 → **526**,套件从 20 → **27** - **ALTER TABLE 语法** — 支持 `ALTER TABLE ... ADD COLUMN` / `DROP COLUMN`(含可选 COLUMN 关键字)
- 新增 7 个模块级测试文件:`aria-page``aria-index``aria-sstable``aria-buffer``aria-wal-mvcc``aria-compress``aria` - **TRUNCATE TABLE 语法** — 支持 `TRUNCATE TABLE name` 快速清空表数据
- LRUList 修复 size 追踪 bug - **MVCC 接入读写路径** — 事务内 insert/update/delete 调用 `mvcc.writeVersion`/`mvcc.deleteVersion`,版本链作为 undo log
- WAL 存储改为按记录独立 key(避免拼接缓冲区越界) - **IndexedDB 索引利用** — `IndexedDBEngine.find` 等值查询时优先使用 IDB 索引(`idx_col` 命名约定),避免全量 getAll
- CheckpointManager 解耦 BufferPool 依赖 - **SQL 注入防护** — React `useTable` / Vue `useSqlarkTable` 增加表名合法性校验(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`
### Fixed ---
- **LZ4 压缩无限循环** — 字面量分支在发现匹配后回退导致 litLen=0 死循环,CI 卡死根因
- **SSTableReader.get() 自比较 bug** — 参数 key 被循环变量遮蔽导致永远返回第一条 ## [0.2.4] - 2026-07-27
- **CheckpointManager 测试 null 引用** — 改为 Mock 对象避免 TypeError 导致进程无法退出
- **IndexedDB 持久化测试** — 替换为 Memory Backend 验证,消除 fake-indexeddb timer 堆积 ### Added
- **LSM.flush() 不必要 setTimeout** — 替换为 Promise.resolve(),消除额外 timer 延迟 - **二级索引** — 每列可独立维护 LSM Tree 索引,`$eq`/`$in`/`$gt`/`$lt` 走索引 O(log n)
- **MVCC 接入引擎** — MVCCManager 正式投产,替换 ad-hoc txnSnapshot
--- - **MVCC 自动 GC** — 每 10 次 checkpoint 自动回收过旧版本(保留最新 100 个)
- **Bloom Filter 序列化** — 写入 SSTable footer + 读取时加载 + 查询时 probe 快速否定
## [0.1.14] - 2026-07-26 - **WAL 大小阈值** — `walSizeThreshold` 配置(默认 16MB),超阈值强制 checkpoint
- **内存预算** — `maxMemoryMB` 配置(默认 64MB
### Fixed
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险 ### Changed
- **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接 - **WAL 默认同步模式** — `walSyncMode` `'batch'` 改为 `'full'`,消除 crash 丢数据风险
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用 - **查询优化** — `tryIndexLookup` 扩展支持非 PK 列索引查找
- 关闭时清理 `onversionchange` 监听器,防止内存泄漏
---
---
## [0.2.3] - 2026-07-27
## [0.1.13] - 2026-07-26
### Fixed
### Added - **RB-Tree fixDelete 完整实现** — 补全标准红黑树删除修复,保证 O(log n)
- **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口 - **LSM SSTable 缓存预热** — `init()` 预加载所有 SSTable,消除 cache miss
- MemoryEngine: 快照式回滚(深拷贝 tables/schemas/indexes - **OPFS 数据恢复** — `open()` 自动从 OPFS 文件加载已有表数据到内存
- IndexedDBEngine: 延迟写入策略(事务中仅写内存,commit 批量刷 IDB) - **Aria WAL 事务恢复** — 两阶段恢复:仅回放已提交事务,未提交数据不回放
- OPFSEngine: 快照式回滚(commit 批量写文件) - **LZ4 格式修复** — 重写 token 格式,消除中间字面量 token 歧义
- HybridEngine: 同时代理内存+磁盘引擎事务
- **子查询支持**: SQL Parser + Executor 支持 `IN (SELECT ...)``op (SELECT ...)` 子查询 ---
- AST 新增 `SubqueryExpression` 类型
- Parser 在 IN 和比较运算符后检测子查询 ## [0.2.2] - 2026-07-27
- Executor 新增 `resolveSubqueries()` 递归解析,自动执行子查询并替换为具体值
- **外键级联操作**: ColumnDef 新增 `onDelete/onUpdate` 选项 ### Added
- 支持 `CASCADE`(递归级联删除)、`SET NULL``RESTRICT` - **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
- SQL Parser 解析 `REFERENCES table(col) ON DELETE CASCADE ON UPDATE CASCADE` - 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()`
- MemoryEngine 内置 `cascadeDelete()` 递归级联逻辑 - 支持 read/write/delete/list/exists/clear 完整接口
- **连接池管理**: `MetonaSqlark.connect()` 静态方法 - 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
- 同名数据库复用已打开实例,引用计数管理 - `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
- `db.disconnect()` 释放连接,归零自动 close
- `MetonaSqlark.disconnectAll()` 强制关闭所有连接 ### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend
### Changed
- `TransactionManager.execute()` 调用引擎层 begin/commit/rollback 实现真正原子性 ---
- IndexedDBEngine CRUD 事务感知:活跃事务中延迟 IDB 写入
- ASTColumnDef / astColumnToColumnDef 支持 references/onDelete/onUpdate 透传 ## [0.2.1] - 2026-07-27
### Fixed ### Fixed
- IndexedDBEngine 事务中 find/count 从内存缓存读取(保证读到未提交变更) - **WAL CRC 校验验证** — 恢复时计算并验证 CRC32,损坏记录自动跳过并告警
- SQL Parser parseColumnDef 循环条件扩展,避免 REFERENCES 语法解析中断 - **Hybrid 引擎提交顺序** — commit 先写磁盘再写内存,磁盘失败回滚内存,消除数据不一致风险
- **RESTRICT 外键行为修正** — 检测到引用行时抛出 `FOREIGN_KEY_VIOLATION`,不再静默保留孤儿
--- - **SSTable rangeScan 边界保护** — 空索引或 startBlockIdx > endBlockIdx 时安全返回
## [0.1.12] - 2026-07-26 ### Added
- **ColumnDef 约束激活** — `maxLength`/`min`/`max` 约束在 `checkFieldType` 中正式生效
### Added - **查询结果上限** — `DatabaseConfig.maxRowsPerQuery`(默认 0 不限制),防止超大结果集 OOM
- `MeSqlark` 别名导出,与 `MetonaSqlark` 完全等价 - **onError 全局回调** — 配置中的 `onError` 回调实际接入 CRUD 异常路径
- `metona-sqlark.cjs.js` CommonJS 构建产物 - **debug 调试模式** — `DatabaseConfig.debug: true` 输出 `[MetonaSqlark:name]` 前缀的结构化日志
- 完善的 `where-matcher.ts` 消除 220+ 行重复代码 - **浏览器兼容性声明** — README 增加 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持矩阵
- `$col` 列引用支持 JOIN ON 条件
- `$and/$or` 嵌套支持(字段级 + 顶层) ### Changed
- `$not` 逻辑非操作符 - MemoryEngine RESTRICT 外键行为:从"不级联保留孤儿"改为"禁止删除抛异常"
- `$like` 正则缓存加速 - Hybrid 引擎 commitTransaction 磁盘优先于内存
- `projectColumns` 列投影支持 `table.column` 格式 - DB_DEFAULTS 新增 `maxRowsPerQuery: 0``debug: false`
- `DISTINCT` 去重支持(O(n) 时间,列值拼接优化)
- 测试覆盖率提升至 93.46%(264 测试/15 套件) ---
### Changed ## [0.2.0] - 2026-07-27
- `where-matcher` 统一 MemoryEngine / IndexedDBEngine / Executor 的 WHERE 逻辑
- JOIN ON 匹配支持 `$col` 语法 ### Added
- LIKE 正则改为缓存式编译 - **AriaEngine 自研存储引擎** — 基于 LSM-Tree 的页面式存储引擎,19 个新模块,~3500 行 TypeScript
- Executor DISTINCT 用列值拼接代替 JSON.stringify - **页面格式层** (`page/`): 4KB Slotted Page 编解码、Tuple 二进制序列化、CRC32 校验
- JOIN 嵌套循环连接优化:避免 ON 时对象扩散 - **Buffer Pool** (`buffer/`): LRU 页面缓存 + 驱逐策略,可控内存占用
- **LSM-Tree 索引** (`index/`): MemTable (红黑树) + 多级 SSTable + Leveled Compaction
### Fixed - **Bloom Filter** (`index/bloom.ts`): FNV-1a + Murmur 双哈希,快速键否定判定
- IndexedDBEngine `update/delete` 方法正确同步内存缓存 - **SSTable 构建器/读取器** (`index/sstable_builder.ts`, `sstable.ts`): 二分查找 + 范围扫描
- OPFSEngine `getTableNames` 兼容 `entries()` 返回 - **Merge Iterator** (`index/merge_iterator.ts`): 最小堆多路归并,去重保留最新
- SQL Parser 正确处理字符串转义字符 - **WAL** (`wal/`): 二进制日志格式 (LSN/type/txnId/table/key/json/CRC) + Checkpoint 管理
- SQL Parser 正确处理表别名(无 AS 关键字) - **MVCC** (`transaction/mvcc.ts`): 版本链 + 快照隔离 + GC
- SQL Parser 解析 `IS NULL` / `IS NOT NULL` / `NOT LIKE` - **存储后端** (`store/`): IndexedDB / Memory 双后端抽象
- `peekTokenIs` 方法正确暴露给语法分析 - **LZ4 压缩** (`compression/lz4.ts`): 简易页面级压缩
- 多处边界条件空值/假值处理 - **`mode: 'aria'`** — 新增存储模式,可通过 `MetonaSqlark.create({ mode: 'aria' })` 激活
- **Schema 持久化** — 表结构自动保存到 `__aria_schemas`,重启自动恢复
--- - **SSTable 元数据管理** — SSTable 索引信息持久化,启动时自动扫描加载
- **事务感知 CRUD** — insert/update/delete 在事务中缓冲到 snapshotcommit 批量写入 LSM
## [0.1.11] - 2026-07-25 - **244 个 AriaEngine 专项测试** — 覆盖生命周期/表管理/CRUD/事务/持久化/SQL 集成/页面格式/LSM/压缩
### Added ### Changed
- React 集成 hooks: `useQuery`, `useTable`, `useDatabase` - `StorageMode` 类型新增 `'aria'`
- Vue 集成 composables: `useSqlarkQuery`, `useSqlarkTable`, `useSqlarkDatabase` - `STORAGE_MODES` 数组新增 `'aria'`
- 发布订阅系统: `subscribe()`, `emit()` - `createEngine()` 支持 `mode: 'aria'` 分支
- 数据迁移系统: `addMigration()`, `migrateTo()` - 测试从 318 → **526**,套件从 20 → **27**
- 导入导出: `exportTable()`, `exportAll()`, `importTable()` - 新增 7 个模块级测试文件:`aria-page``aria-index``aria-sstable``aria-buffer``aria-wal-mvcc``aria-compress``aria`
- 完整 SQL 解析器: `Lexer` + `Parser`(递归下降) - LRUList 修复 size 追踪 bug
- SQL 支持: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE - WAL 存储改为按记录独立 key(避免拼接缓冲区越界)
- JOIN 支持: INNER, LEFT, RIGHT, CROSS - CheckpointManager 解耦 BufferPool 依赖
- GROUP BY + HAVING + 聚合函数 (COUNT, SUM, AVG, MIN, MAX)
- DISTINCT, ORDER BY, LIMIT, OFFSET ### Fixed
- WHERE 条件: AND, OR, NOT, IN, LIKE, IS NULL - **LZ4 压缩无限循环** — 字面量分支在发现匹配后回退导致 litLen=0 死循环,CI 卡死根因
- Query Builder 链式 API: `select().where().orderBy().limit().execute()` - **SSTableReader.get() 自比较 bug** — 参数 key 被循环变量遮蔽导致永远返回第一条
- 插件系统: 14 种生命周期钩子 + PluginManager - **CheckpointManager 测试 null 引用** — 改为 Mock 对象避免 TypeError 导致进程无法退出
- Metadata 系统: ColumnDef 完整约束 (type/primaryKey/required/unique/index/default/references/maxLength/min/max) - **IndexedDB 持久化测试** — 替换为 Memory Backend 验证,消除 fake-indexeddb timer 堆积
- **LSM.flush() 不必要 setTimeout** — 替换为 Promise.resolve(),消除额外 timer 延迟
### 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.1.14] - 2026-07-26
--- ### Fixed
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险
## [0.0.1] - 2026-07-24 - **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用
### Added - 关闭时清理 `onversionchange` 监听器,防止内存泄漏
- 初始项目骨架
- TypeScript 配置(严格模式) ---
- Rollup 构建(UMD / ESM / CJS / min / .d.ts
- Jest + jsdom 测试环境 ## [0.1.13] - 2026-07-26
- ESLint + @typescript-eslint
- Gitea Actions CI 工作流 ### Added
- 示例站点(index / demo / docs - **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口
- 构建脚本 build.sh - 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 ├── core.ts # MetonaSqlark main class
├── constants.ts # Types, defaults, enums, errors ├── constants.ts # Types, defaults, enums, errors
├── utils.ts # Utility functions ├── utils.ts # Utility functions
├── connection-manager.ts # Connection pool (connect/disconnect)
├── engine/ # Storage engines ├── engine/ # Storage engines
│ ├── interface.ts # IStorageEngine interface │ ├── interface.ts # IStorageEngine interface
│ ├── memory.ts # MemoryEngine (Map-based) │ ├── memory.ts # MemoryEngine (Map-based)
│ ├── indexeddb.ts # IndexedDBEngine (browser persistence) │ ├── 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) ├── hybrid/ # HybridEngine (write-through)
├── table/ # Table management & Schema validation ├── table/ # Table management & Schema validation
├── query/ # Query system ├── query/ # Query system
@@ -64,7 +73,7 @@ src/
├── plugin/ # Plugin system (14 lifecycle hooks) ├── plugin/ # Plugin system (14 lifecycle hooks)
└── integrations/ # React & Vue 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) site/ # Documentation site (index / docs / demo)
``` ```
@@ -133,9 +142,9 @@ npm run build
# Output in dist/ # Output in dist/
# ├── metona-sqlark.js UMD # ├── 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.esm.js ES Module
# ├── metona-sqlark.cjs.js CommonJS # ├── metona-sqlark.cjs CommonJS
# └── metona-sqlark.d.ts TypeScript declarations # └── metona-sqlark.d.ts TypeScript declarations
``` ```
+34 -13
View File
@@ -1,10 +1,10 @@
# MetonaSqlark # MetonaSqlark
<p align="center"> <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/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/coverage-81.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/tests-761%20passed-success?style=flat-square" alt="tests">
</p> </p>
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。 > 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。
@@ -18,13 +18,13 @@
- 🔒 **生产级数据安全** — WAL CRC 完整性校验、`RESTRICT` 外键约束、Hybrid 提交原子性、SQL 注入防护 - 🔒 **生产级数据安全** — WAL CRC 完整性校验、`RESTRICT` 外键约束、Hybrid 提交原子性、SQL 注入防护
- 🛡 **输入校验全覆盖**`maxLength`/`min`/`max` 约束、类型检查、必填验证 - 🛡 **输入校验全覆盖**`maxLength`/`min`/`max` 约束、类型检查、必填验证
- 💾 **多引擎架构** — Memory / IndexedDB / OPFS / Hybrid(write-through) / Aria 五种模式 - 💾 **多引擎架构** — 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()` - 🔗 **Query Builder API** — 链式 `.select().where().orderBy().limit().execute()`
- 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径 - 🔄 **事务回滚** — Memory/IndexedDB/Hybrid/Aria 四引擎事务原子性,自动回滚,MVCC 版本链接入读写路径
- 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证 - 🌲 **RB-Tree 完整实现** — 标准红黑树插入+删除修复,O(log n) 保证
-**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态 -**性能优化** — SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化避免全局状态
- 🌐 **浏览器兼容** — Chrome 80+ / Firefox 80+ / Safari 14+ / Edge 80+ / Node.js 16+ - 🌐 **浏览器兼容** — 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/) 目录下载: 或从 [`dist/`](./dist/) 目录下载:
- `metona-sqlark.js` — UMD 开发版(含 sourcemap - `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.esm.js` — ES Module
- `metona-sqlark.cjs.js` — CommonJS - `metona-sqlark.cjs` — CommonJS
- `metona-sqlark.d.ts` — TypeScript 类型声明 - `metona-sqlark.d.ts` — TypeScript 类型声明
--- ---
@@ -98,7 +98,7 @@ import { MetonaSqlark } from '@metona-team/metona-sqlark';
const db = await MetonaSqlark.create({ const db = await MetonaSqlark.create({
name: 'my-app', name: 'my-app',
mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' mode: 'hybrid', // 'memory' | 'disk' | 'hybrid' | 'aria'
diskEngine: 'indexeddb', // 'indexeddb' | 'opfs' diskEngine: 'indexeddb', // 'indexeddb' | 'opfs'
}); });
@@ -144,6 +144,27 @@ await db.query('ALTER TABLE users DROP COLUMN phone');
// TRUNCATE TABLE — 快速清空表 v0.2.5 // TRUNCATE TABLE — 快速清空表 v0.2.5
await db.query('TRUNCATE TABLE old_logs'); 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 // 事务 — 自动回滚 v0.1.13
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '3', name: 'Charlie' }); 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 的设计理念: AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念:
@@ -326,7 +347,7 @@ const rows = await db.query('SELECT * FROM users');
| **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction,写背压 | | **LSM-Tree** | MemTable (红黑树) → SSTable 多级索引,异步 Compaction,写背压 |
| **WAL** | Write-Ahead Log 二进制格式,CRC 校验,full/batch/none 三种模式(full 模式真正同步 ✅ v0.2.5),16MB 阈值自动 checkpoint | | **WAL** | Write-Ahead Log 二进制格式,CRC 校验,full/batch/none 三种模式(full 模式真正同步 ✅ v0.2.5),16MB 阈值自动 checkpoint |
| **MVCC** | 版本链 + 快照隔离,事务读写不互斥,自动 GC(每10次检查点),读写路径接入版本链 ✅ v0.2.5 | | **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 | | **Bloom Filter** | FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时 probe |
| **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,SSTableReader 二分查找统一 ✅ v0.2.5 | | **二级索引** | 每列独立 LSM Tree,支持 $eq/$in/$gt/$lt 范围扫描,SSTableReader 二分查找统一 ✅ v0.2.5 |
| **AES-GCM** | PBKDF2 密钥派生 + AES-256-GCM 页面级加密,CryptoManager 实例化 ✅ v0.2.5 | | **AES-GCM** | PBKDF2 密钥派生 + AES-256-GCM 页面级加密,CryptoManager 实例化 ✅ v0.2.5 |
@@ -352,9 +373,9 @@ npm run typecheck # 类型检查
| 指标 | 数值 | | 指标 | 数值 |
|------|------| |------|------|
| 测试用例 | 721 | | 测试用例 | 761 |
| 测试套件 | 37 | | 测试套件 | 41 |
| 行覆盖率 | 91.0% | | 行覆盖率 | 81.0% |
| SQL 关键字 | 36 | | SQL 关键字 | 36 |
| 存储引擎 | 5Memory / IndexedDB / OPFS / Hybrid / **Aria** | | 存储引擎 | 5Memory / IndexedDB / OPFS / Hybrid / **Aria** |
+9 -9
View File
@@ -1,9 +1,9 @@
module.exports = { module.exports = {
presets: [ presets: [
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }], ['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-typescript', '@babel/preset-typescript',
], ],
plugins: [ plugins: [
'@babel/plugin-transform-modules-commonjs', '@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; maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */ /** 调试模式(启用后输出详细操作日志) */
debug?: boolean; debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
} }
/** Where 条件操作符 */ /** Where 条件操作符 */
type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not'; type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
@@ -114,7 +116,7 @@ interface MetonaPlugin {
/** 销毁 */ /** 销毁 */
destroy(): void; destroy(): void;
} }
declare const VERSION = "0.2.5"; declare const VERSION = "0.3.2";
/** /**
* metona-sqlark Plugin — 插件系统 * metona-sqlark Plugin — 插件系统
@@ -179,6 +181,10 @@ interface IStorageEngine {
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
/** 清空表数据(保留结构) */ /** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
/** 提交事务 */ /** 提交事务 */
@@ -254,7 +260,10 @@ interface InsertStatement {
type: 'INSERT'; type: 'INSERT';
into: string; into: string;
columns?: string[]; columns?: string[];
values: unknown[][]; /** VALUES 字面量 */
values?: unknown[][];
/** INSERT INTO ... SELECT ...v0.3.0 */
select?: SelectStatement | SelectUnionStatement;
} }
interface UpdateStatement { interface UpdateStatement {
type: 'UPDATE'; type: 'UPDATE';
@@ -285,6 +294,15 @@ interface SelectStatement {
limit?: number; limit?: number;
offset?: number; offset?: number;
} }
interface SelectUnionStatement {
type: 'SELECT_UNION';
/** 左操作数(可以是 SELECT 或嵌套 UNION */
left: SelectStatement | SelectUnionStatement;
/** 右操作数 */
right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
all?: boolean;
}
interface AlterTableStatement { interface AlterTableStatement {
type: 'ALTER_TABLE'; type: 'ALTER_TABLE';
name: string; name: string;
@@ -295,7 +313,31 @@ interface TruncateTableStatement {
type: 'TRUNCATE_TABLE'; type: 'TRUNCATE_TABLE';
name: string; 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 执行器 * metona-sqlark Query Executor — AST 执行器
@@ -311,11 +353,23 @@ declare class QueryExecutor {
/** 设置查询结果行数上限 */ /** 设置查询结果行数上限 */
setMaxRowsPerQuery(max: number): void; setMaxRowsPerQuery(max: number): void;
execute(stmt: Statement): Promise<unknown>; execute(stmt: Statement): Promise<unknown>;
/** 递归执行 UNION / UNION ALL,返回合并结果 */
private executeSelectUnion;
private executeSelectPart;
/** 将 UNION 右侧行投影为左侧列结构(按位置取值) */
private projectUnionRow;
/** EXPLAIN: 输出查询计划 */ /** EXPLAIN: 输出查询计划 */
private executeExplain; private executeExplain;
private executeSelect; private executeSelect;
private executeJoinSelect; private executeJoinSelect;
private prefixRow; private prefixRow;
/**
* 哈希连接(v0.3.2):ON 为单一等值条件且右表列为索引/主键时,
* 收集左表连接值 → 一次 $in 查询右表 → 哈希映射匹配。
* 替代嵌套循环,大表 INNER/LEFT JOIN 复杂度 O(N + M)。
* 不适用时返回 null(回退嵌套循环)。
*/
private tryHashJoin;
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */ /** 嵌套循环连接(优化:避免 ON 时对象扩散) */
private joinRows; private joinRows;
private executeGroupBy; private executeGroupBy;
@@ -328,14 +382,47 @@ declare class QueryExecutor {
private executeDropTable; private executeDropTable;
private executeAlterTable; private executeAlterTable;
private executeTruncateTable; private executeTruncateTable;
private executeCreateIndex;
private executeDropIndex;
private executeBegin;
private executeCommit;
private executeRollback;
/** 列列表是否包含 CASE WHEN 表达式 */
private hasCaseColumn;
/** WHERE 是否包含 CASE WHEN 表达式键 */
private whereHasCase;
getEngine(): IStorageEngine; getEngine(): IStorageEngine;
/**
* 列投影(v0.3.1):普通列走 projectColumnsCASE WHEN 表达式逐行求值
*/
private projectRow;
/** 检查 SELECT 列列表中是否包含聚合函数 */ /** 检查 SELECT 列列表中是否包含聚合函数 */
private _hasAggregateColumn; private _hasAggregateColumn;
/** 计算单行聚合结果(无 GROUP BY) */ /** 计算单行聚合结果(无 GROUP BY) */
private computeSingleAggregate; 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 标记并执行子查询, * 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
* 将结果替换为具体值。 * 将结果替换为具体值。
* @param contextRow 关联子查询的外层行上下文(用于绑定 $col 引用)
*/ */
private resolveSubqueries; private resolveSubqueries;
/** /**
@@ -395,7 +482,8 @@ declare class UpdateQueryBuilder {
private tableName; private tableName;
private _updates; private _updates;
private _where; 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; where(condition: WhereCondition): this;
execute(): Promise<number>; execute(): Promise<number>;
toAST(): UpdateStatement; toAST(): UpdateStatement;
@@ -404,7 +492,8 @@ declare class DeleteQueryBuilder {
private engine; private engine;
private tableName; private tableName;
private _where; private _where;
constructor(engine: IStorageEngine, tableName: string); private onWrite?;
constructor(engine: IStorageEngine, tableName: string, onWrite?: (table: string) => void);
where(condition: WhereCondition): this; where(condition: WhereCondition): this;
execute(): Promise<number>; execute(): Promise<number>;
toAST(): DeleteStatement; toAST(): DeleteStatement;
@@ -420,7 +509,9 @@ declare class Table<T = Record<string, unknown>> {
private engine; private engine;
private schema; private schema;
private executor; 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>; getSchema(): Promise<TableSchema>;
insert(row: T & Record<string, unknown>): Promise<string>; insert(row: T & Record<string, unknown>): Promise<string>;
insertMany(rows: (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 maxRowsPerQuery(): number;
/** 调试模式 */ /** 调试模式 */
get debug(): boolean; get debug(): boolean;
/** 多标签页同步通道(v0.3.2 */
private channel;
constructor(config: DatabaseConfig); constructor(config: DatabaseConfig);
/** 初始化数据库(创建引擎、打开连接) */ /** 初始化数据库(创建引擎、打开连接) */
init(): Promise<void>; init(): Promise<void>;
@@ -500,12 +593,18 @@ declare class MetonaSqlark {
subscribe(tableName: string, callback: (event: { subscribe(tableName: string, callback: (event: {
type: string; type: string;
row?: unknown; row?: unknown;
table?: string;
}) => void): () => void; }) => void): () => void;
/** 触发变更事件 */ /** 触发变更事件 */
emit(tableName: string, event: { emit(tableName: string, event: {
type: string; type: string;
row?: unknown; row?: unknown;
table?: string;
}): void; }): void;
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName: string): void;
/** 写语句对应的表名(多标签页广播用) */
private writeStatementTable;
private migrations; private migrations;
/** 注册迁移 */ /** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void; 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>; delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>; commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>; rollbackTransaction(): Promise<void>;
@@ -590,6 +691,12 @@ declare class IndexedDBEngine implements IStorageEngine {
private memoryCache; private memoryCache;
private txActive; private txActive;
open(dbName: string, version: number): Promise<void>; open(dbName: string, version: number): Promise<void>;
/**
* 从 IDB 恢复内存 schema
* 1. 优先读取持久化的 schema 记录('__metona_schema' storev0.3.2
* 2. 旧数据回退:从 objectStore 主键 / 索引 / 样例数据推断
*/
private rebuildSchemaFromIDB;
close(): Promise<void>; close(): Promise<void>;
isOpen(): boolean; isOpen(): boolean;
createTable(schema: TableSchema): Promise<void>; createTable(schema: TableSchema): Promise<void>;
@@ -603,6 +710,8 @@ declare class IndexedDBEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>; delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>; commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>; rollbackTransaction(): Promise<void>;
@@ -648,6 +757,8 @@ declare class OPFSEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>; delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>; commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>; rollbackTransaction(): Promise<void>;
@@ -727,6 +838,8 @@ declare class AriaEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>; delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>; commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>; rollbackTransaction(): Promise<void>;
@@ -741,6 +854,14 @@ declare class AriaEngine implements IStorageEngine {
private checkType; private checkType;
private persistSchemas; private persistSchemas;
private loadSchemas; 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 createSSTableStore;
private applyWALRecord; private applyWALRecord;
/** 更新行的二级索引条目 */ /** 更新行的二级索引条目 */
@@ -751,6 +872,8 @@ declare class AriaEngine implements IStorageEngine {
private indexScanToRows; private indexScanToRows;
/** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */ /** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */
private tryGC; private tryGC;
/** 回收主 LSM 与所有二级索引 LSM 的临时缓存超限 */
private trimAllCaches;
/** 检查内存预算,超出时强制 flush + GC */ /** 检查内存预算,超出时强制 flush + GC */
private checkMemoryBudget; private checkMemoryBudget;
/** 估算 WAL 大小(字节) */ /** 估算 WAL 大小(字节) */
@@ -801,8 +924,15 @@ declare class HybridEngine implements IStorageEngine {
private memoryEngine; private memoryEngine;
private diskEngine; private diskEngine;
private diskEngineType; private diskEngineType;
private dbName;
private version;
constructor(diskEngine?: DiskEngine); constructor(diskEngine?: DiskEngine);
open(dbName: string, version: number): Promise<void>; open(dbName: string, version: number): Promise<void>;
/**
* 从磁盘重载内存缓存(v0.3.2:多标签页同步)。
* 其他标签页写入磁盘后调用,使本标签页读到最新数据。
*/
reloadMemoryFromDisk(): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
isOpen(): boolean; isOpen(): boolean;
createTable(schema: TableSchema): Promise<void>; createTable(schema: TableSchema): Promise<void>;
@@ -816,6 +946,8 @@ declare class HybridEngine implements IStorageEngine {
delete(tableName: string, query: QueryPlan): Promise<number>; delete(tableName: string, query: QueryPlan): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>; count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>; 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>; beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>; commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>; rollbackTransaction(): Promise<void>;
@@ -835,6 +967,8 @@ declare class HybridEngine implements IStorageEngine {
/** 解析 SQL 字符串为 AST Statement */ /** 解析 SQL 字符串为 AST Statement */
declare function parse(sql: string): Statement; declare function parse(sql: string): Statement;
/** 解析 SQL 字符串为 AST Statement 数组(分号分隔的多语句支持,v0.3.0) */
declare function parseAll(sql: string): Statement[];
/** /**
* metona-sqlark SQL Token Types — 词法单元定义 * metona-sqlark SQL Token Types — 词法单元定义
@@ -895,6 +1029,17 @@ declare enum TokenType {
MIN = "MIN", MIN = "MIN",
MAX = "MAX", MAX = "MAX",
DISTINCT = "DISTINCT", 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", IDENTIFIER = "IDENTIFIER",
STRING = "STRING", STRING = "STRING",
NUMBER = "NUMBER", NUMBER = "NUMBER",
@@ -1028,4 +1173,4 @@ declare global {
declare const MeSqlark: typeof MetonaSqlark; 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 = { module.exports = {
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
setupFiles: ['./jest.setup.js'], setupFiles: ['./jest.setup.js'],
transform: { transform: {
'^.+\\.ts$': 'babel-jest', '^.+\\.ts$': 'babel-jest',
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)', '/node_modules/(?!(@rollup)/)',
], ],
moduleFileExtensions: ['ts', 'js', 'json'], moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [ collectCoverageFrom: [
'src/**/*.ts', 'src/**/*.ts',
'!src/index.ts', '!src/index.ts',
'!src/**/index.ts', '!src/**/index.ts',
'!src/query/ast.ts', '!src/query/ast.ts',
'!src/engine/interface.ts', '!src/engine/interface.ts',
'!src/engine/opfs.ts', '!src/engine/opfs.ts',
'!src/integrations/**', '!src/integrations/**',
], ],
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'], coverageReporters: ['text', 'lcov'],
verbose: true, verbose: true,
// Must have: fake-indexeddb + debounce timers keep event loop alive // Must have: fake-indexeddb + debounce timers keep event loop alive
forceExit: true, forceExit: true,
testTimeout: 15000, testTimeout: 15000,
}; };
+27 -15
View File
@@ -1,15 +1,27 @@
// jest setup: polyfill structuredClone for fake-indexeddb // jest setup: polyfill structuredClone for fake-indexeddb
if (typeof globalThis.structuredClone !== 'function') { if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj)); globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
} }
// polyfill TextEncoder/TextDecoder for jsdom environment // polyfill TextEncoder/TextDecoder for jsdom environment
if (typeof globalThis.TextEncoder === 'undefined') { if (typeof globalThis.TextEncoder === 'undefined') {
const { TextEncoder: TE, TextDecoder: TD } = require('util'); const { TextEncoder: TE, TextDecoder: TD } = require('util');
globalThis.TextEncoder = TE; globalThis.TextEncoder = TE;
globalThis.TextDecoder = TD; globalThis.TextDecoder = TD;
} }
if (typeof globalThis.TextDecoder === 'undefined') { if (typeof globalThis.TextDecoder === 'undefined') {
const { TextDecoder: TD } = require('util'); const { TextDecoder: TD } = require('util');
globalThis.TextDecoder = TD; 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", "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", "description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module", "type": "module",
"main": "dist/metona-sqlark.js", "main": "dist/metona-sqlark.cjs",
"module": "src/index.ts", "module": "dist/metona-sqlark.esm.js",
"unpkg": "dist/metona-sqlark.min.js", "unpkg": "dist/metona-sqlark.min.js",
"jsdelivr": "dist/metona-sqlark.min.js", "jsdelivr": "dist/metona-sqlark.min.js",
"types": "dist/metona-sqlark.d.ts", "types": "dist/metona-sqlark.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./src/index.ts", "import": "./dist/metona-sqlark.esm.js",
"require": "./dist/metona-sqlark.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" "types": "./dist/metona-sqlark.d.ts"
} }
}, },
+7 -2
View File
@@ -40,6 +40,8 @@ export default [
exports: 'named', exports: 'named',
sourcemap: true, sourcemap: true,
}, },
// react/vue 为 peer dependencyintegrations 子路径),不打包
external: ['react', 'vue'],
plugins: [...basePlugins, ...devPlugins], plugins: [...basePlugins, ...devPlugins],
}, },
// Only build all formats in production // Only build all formats in production
@@ -53,17 +55,19 @@ export default [
exports: 'named', exports: 'named',
sourcemap: true, sourcemap: true,
}, },
external: ['react', 'vue'],
plugins: basePlugins, plugins: basePlugins,
}, },
// CommonJS // CommonJS.cjs 后缀:package.json 为 type:module.js 会被 Node 按 ESM 解析)
{ {
input: 'src/index.ts', input: 'src/index.ts',
output: { output: {
file: 'dist/metona-sqlark.cjs.js', file: 'dist/metona-sqlark.cjs',
format: 'cjs', format: 'cjs',
exports: 'named', exports: 'named',
sourcemap: true, sourcemap: true,
}, },
external: ['react', 'vue'],
plugins: basePlugins, plugins: basePlugins,
}, },
// UMD minified // UMD minified
@@ -76,6 +80,7 @@ export default [
exports: 'named', exports: 'named',
sourcemap: false, sourcemap: false,
}, },
external: ['react', 'vue'],
plugins: [ plugins: [
...basePlugins, ...basePlugins,
terser({ 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> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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>"> <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> <style>
:root { :root {
@@ -82,14 +82,15 @@
<a href="index.html">首页</a> <a href="index.html">首页</a>
<a href="docs.html">文档</a> <a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a> <a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a>
</nav> </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> </header>
<div class="main"> <div class="main">
<div class="editor-panel"> <div class="editor-panel">
<div class="editor-area"> <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 表数据 -- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护 -- 新特性: 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('adv')">🧪 高级</button>
<button class="btn btn-preset" onclick="loadPreset('alter')">🏗 ALTER</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('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> <button class="btn btn-preset" onclick="loadPreset('aria')" style="color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</div> </div>
</div> </div>
@@ -511,9 +518,9 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理 -- 清理
DROP TABLE temp_logs;`, DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.2.5) aria: `-- 🌲 AriaEngine 演示 (v0.2.5 → v0.3.2)
-- AriaEngine: LSM-Tree 自研存储引擎 -- AriaEngine: LSM-Tree 自研存储引擎
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 721测试 -- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 836测试
-- 基础 CRUD 完全兼容 -- 基础 CRUD 完全兼容
CREATE TABLE IF NOT EXISTS tasks ( CREATE TABLE IF NOT EXISTS tasks (
@@ -533,18 +540,133 @@ DROP TABLE temp_logs;`,
-- 聚合统计 -- 聚合统计
SELECT done, COUNT(*) as cnt FROM tasks GROUP BY done; 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 特性: -- AriaEngine 特性:
-- • LSM-Tree: MemTable (红黑树) → SSTable 多级索引 -- • LSM-Tree: MemTable (红黑树) → SSTable 多级索引
-- • WAL: Write-Ahead Log 保证崩溃恢复 -- • WAL: Write-Ahead Log 保证崩溃恢复 + 批量组提交
-- • MVCC: 版本链 + 快照隔离 -- • MVCC: 版本链 + 快照隔离
-- • Buffer Pool: LRU 页面缓存 (256页 ~ 1MB) -- • Buffer Pool: SSTable LRU 缓存 (256页 ~ 1MB) ✅ 已生效
-- • Bloom Filter: FNV-1a + Murmur 双哈希 -- • Bloom Filter: FNV-1a + Murmur 双哈希
-- • Slotted Page: 4KB 页面 + Tuple 二进制编码 -- • 二级索引: 每列独立 LSM + 动态 CREATE INDEX
-- 生产环境: mode: 'aria' 激活自研引擎 -- 生产环境: mode: 'aria' 激活自研引擎
-- const db = await MetonaSqlark.create({ -- const db = await MetonaSqlark.create({
-- name: 'my-app', mode: 'aria' -- 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) { function loadPreset(name) {
@@ -564,7 +686,7 @@ document.addEventListener('keydown', e => {
// Boot // Boot
initDB().then(() => { initDB().then(() => {
console.log('✅ MetonaSqlark v0.2.5 demo ready'); console.log('✅ MetonaSqlark v0.3.2 demo ready');
setTimeout(runQuery, 300); setTimeout(runQuery, 300);
}).catch(err => { }).catch(err => {
renderError('初始化失败: ' + err.message); renderError('初始化失败: ' + err.message);
+58 -6
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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>"> <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> <style>
:root { :root {
@@ -69,6 +69,7 @@
<a href="index.html">首页</a> <a href="index.html">首页</a>
<a href="docs.html" class="nav-active">文档</a> <a href="docs.html" class="nav-active">文档</a>
<a href="demo.html">演示</a> <a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
</nav> </nav>
</div> </div>
</header> </header>
@@ -121,7 +122,7 @@ npm install @metona-team/metona-sqlark</pre>
<h3>CDN / UMD</h3> <h3>CDN / UMD</h3>
<pre><span class="c">&lt;!-- UMD 格式,暴露 window.MetonaSqlark 和 window.MeSqlark --&gt;</span> <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; &lt;script&gt;
<span class="k">const</span> db = <span class="k">await</span> window.<span class="f">MetonaSqlark</span>.create({...}); <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> <span class="c">// 或 window.MeSqlark.create(...) — 完全等价</span>
@@ -138,9 +139,9 @@ npm install @metona-team/metona-sqlark</pre>
<table> <table>
<tr><th>文件</th><th>格式</th><th>用途</th></tr> <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.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.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> <tr><td><code>metona-sqlark.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr>
</table> </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="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> <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> <h3>条件表达式</h3>
<pre><span class="c">// 比较运算符</span> <pre><span class="c">// 比较运算符</span>
<span class="s">`WHERE age > 18 AND name LIKE 'A%'`</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="c">// 取消订阅</span>
<span class="f">unsubscribe</span>();</pre> <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> <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>; <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>; <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>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>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>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> </table>
<h2 id="engine">💾 存储引擎</h2> <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> <h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br> <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.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 761测试 · 零死代码。<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.3.2 表达式与并发</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 761测试 41套件。</p>
<h3>存储模式对比</h3> <h3>存储模式对比</h3>
<table> <table>
+7 -6
View File
@@ -143,6 +143,7 @@
<a href="index.html">首页</a> <a href="index.html">首页</a>
<a href="docs.html">文档</a> <a href="docs.html">文档</a>
<a href="demo.html">演示</a> <a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a> <a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a>
</nav> </nav>
<a href="demo.html" class="btn btn-primary">立即体验</a> <a href="demo.html" class="btn btn-primary">立即体验</a>
@@ -152,7 +153,7 @@
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="container"> <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> <h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p> <p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions"> <div class="actions">
@@ -242,7 +243,7 @@ npm install @metona-team/metona-sqlark
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">🌲</div> <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> <p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL full模式真正同步、MVCC 版本链接入读写路径。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
@@ -273,7 +274,7 @@ npm install @metona-team/metona-sqlark
<div class="feature-card"> <div class="feature-card">
<div class="icon">📦</div> <div class="icon">📦</div>
<h3>零运行时依赖</h3> <h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~10KB gzip。</p> <p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~27KB gzip。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">📊</div> <div class="icon">📊</div>
@@ -393,9 +394,9 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p> <p>MetonaSqlark 的核心指标</p>
</div> </div>
<div class="stats"> <div class="stats">
<div class="stat-card"><div class="num">721</div><div class="label">测试用例</div></div> <div class="stat-card"><div class="num">761</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">81.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">~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">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">36</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">37</div><div class="label">测试套件</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 / / * metona-sqlark Constants / /
* @module constants * @module constants
*/ */
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 存储模式 // 存储模式
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** 存储模式 */ /** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria'; export type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型 */ /** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs'; export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */ /** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid', 'aria']; export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid', 'aria'];
/** 所有磁盘引擎 */ /** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs']; export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 字段类型 // 字段类型
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** 字段数据类型 */ /** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json'; export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */ /** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json']; export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 列定义 & 表结构 // 列定义 & 表结构
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** 列定义 */ /** 列定义 */
export interface ColumnDef { export interface ColumnDef {
/** 字段类型 */ /** 字段类型 */
type: FieldType; type: FieldType;
/** 是否主键 */ /** 是否主键 */
primaryKey?: boolean; primaryKey?: boolean;
/** 是否必填 */ /** 是否必填 */
required?: boolean; required?: boolean;
/** 是否唯一 */ /** 是否唯一 */
unique?: boolean; unique?: boolean;
/** 默认值 */ /** 默认值 */
default?: unknown; default?: unknown;
/** 是否创建索引 */ /** 是否创建索引 */
index?: boolean; index?: boolean;
/** 外键引用: 'table.column' */ /** 外键引用: 'table.column' */
references?: string; references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */ /** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */ /** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */ /** 字符串最大长度 */
maxLength?: number; maxLength?: number;
/** 数字最小值 */ /** 数字最小值 */
min?: number; min?: number;
/** 数字最大值 */ /** 数字最大值 */
max?: number; max?: number;
} }
/** 表结构定义 */ /** 表结构定义 */
export interface TableSchema { export interface TableSchema {
/** 表名 */ /** 表名 */
name: string; name: string;
/** 列定义映射 */ /** 列定义映射 */
columns: Record<string, ColumnDef>; columns: Record<string, ColumnDef>;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 数据库配置 // 数据库配置
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** 数据库配置 */ /** 数据库配置 */
export interface DatabaseConfig { export interface DatabaseConfig {
/** 数据库名称 */ /** 数据库名称 */
name: string; name: string;
/** 存储模式 */ /** 存储模式 */
mode?: StorageMode; mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */ /** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine; diskEngine?: DiskEngine;
/** 版本号 */ /** 版本号 */
version?: number; version?: number;
/** 插件列表 */ /** 插件列表 */
plugins?: MetonaPlugin[]; plugins?: MetonaPlugin[];
/** 数据库就绪回调 */ /** 数据库就绪回调 */
onReady?: (db: unknown) => void; onReady?: (db: unknown) => void;
/** 错误回调 */ /** 错误回调 */
onError?: (error: Error) => void; onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */ /** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number; maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */ /** 调试模式(启用后输出详细操作日志) */
debug?: boolean; 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, export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
diskEngine: 'indexeddb' as const, name: 'metona-sqlark',
version: 1, mode: 'hybrid' as const,
maxRowsPerQuery: 0, // 0 = 不限制 diskEngine: 'indexeddb' as const,
debug: false, version: 1,
}); maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
// --------------------------------------------------------------------------- multiTabSync: false,
// Where 操作符 });
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** Where 条件操作符 */ // Where 操作符
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not'; // ---------------------------------------------------------------------------
/** 简单条件值:直接相等 */ /** Where 条件操作符 */
export type SimpleCondition = unknown; export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 操作符条件 */ /** 简单条件值:直接相等 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>; export type SimpleCondition = unknown;
/** 字段条件:简单值 | 操作符对象 */ /** 操作符条件 */
export type FieldCondition = SimpleCondition | OperatorCondition; export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** Where 条件对象 */ /** 字段条件:简单值 | 操作符对象 */
export type WhereCondition = Record<string, FieldCondition>; export type FieldCondition = SimpleCondition | OperatorCondition;
// --------------------------------------------------------------------------- /** Where 条件对象 */
// 排序 export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** 排序方向 */ // 排序
export type SortDirection = 'asc' | 'desc'; // ---------------------------------------------------------------------------
/** 排序定义 */ /** 排序方向 */
export interface OrderBy { export type SortDirection = 'asc' | 'desc';
/** 列名 */
column: string; /** 排序定义 */
/** 排序方向 */ export interface OrderBy {
direction: SortDirection; /** 列名 */
} column: string;
/** 排序方向 */
// --------------------------------------------------------------------------- direction: SortDirection;
// 查询计划(引擎层使用) }
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */ // 查询计划(引擎层使用)
export interface QueryPlan { // ---------------------------------------------------------------------------
/** 表名 */
table: string; /** 查询计划 — 由 Executor 编译 AST 后生成 */
/** 要返回的列(undefined = 全部,['*'] = 全部) */ export interface QueryPlan {
columns?: string[]; /** 表名 */
/** 过滤条件 */ table: string;
where?: WhereCondition; /** 要返回的列(undefined = 全部,['*'] = 全部) */
/** 排序 */ columns?: string[];
orderBy?: OrderBy[]; /** 过滤条件 */
/** 限制条数 */ where?: WhereCondition;
limit?: number; /** 排序 */
/** 偏移量 */ orderBy?: OrderBy[];
offset?: number; /** 限制条数 */
} limit?: number;
/** 偏移量 */
// --------------------------------------------------------------------------- offset?: number;
// 插件接口 }
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** 钩子名称 */ // 插件接口
export type HookName = // ---------------------------------------------------------------------------
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable' /** 钩子名称 */
| 'beforeInsert' | 'afterInsert' export type HookName =
| 'beforeUpdate' | 'afterUpdate' | 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDelete' | 'afterDelete' | 'beforeDropTable' | 'afterDropTable'
| 'beforeQuery' | 'afterQuery' | 'beforeInsert' | 'afterInsert'
| 'beforeTransaction' | 'afterTransaction'; | 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
/** 插件定义 */ | 'beforeQuery' | 'afterQuery'
export interface MetonaPlugin { | 'beforeTransaction' | 'afterTransaction';
/** 插件名称 */
name: string; /** 插件定义 */
/** 插件版本 */ export interface MetonaPlugin {
version: string; /** 插件名称 */
/** 描述 */ name: string;
description?: string; /** 插件版本 */
/** 优先级,越大越先执行 */ version: string;
priority?: number; /** 描述 */
/** 安装 */ description?: string;
install(db: unknown): void; /** 优先级,越大越先执行 */
/** 销毁 */ priority?: number;
destroy(): void; /** 安装 */
} install(db: unknown): void;
/** 销毁 */
// --------------------------------------------------------------------------- destroy(): void;
// 错误类型 }
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** 数据库错误 */ // 错误类型
export class DatabaseError extends Error { // ---------------------------------------------------------------------------
constructor(
message: string, /** 数据库错误 */
public code: string, export class DatabaseError extends Error {
public details?: unknown, constructor(
) { message: string,
super(message); public code: string,
this.name = 'DatabaseError'; public details?: unknown,
} ) {
} super(message);
this.name = 'DatabaseError';
// --------------------------------------------------------------------------- }
// 版本 }
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
export const VERSION = '0.2.5'; // 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.3.2';
+391 -329
View File
@@ -1,329 +1,391 @@
/** /**
* metona-sqlark Core * metona-sqlark Core
* @module core * @module core
* *
* SQL * SQL
*/ */
import type { IStorageEngine } from './engine/interface'; import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants'; import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants'; import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory'; import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb'; import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs'; import { OPFSEngine } from './engine/opfs';
import { AriaEngine } from './engine/aria/index'; import { AriaEngine } from './engine/aria/index';
import { HybridEngine } from './hybrid/index'; import { HybridEngine } from './hybrid/index';
import { Table } from './table/table'; import { Table } from './table/table';
import { createSchema } from './table/schema'; import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor'; import { QueryExecutor } from './query/executor';
import { parse } from './sql/parser'; import { parseAll } from './sql/parser';
import { TransactionManager } from './transaction/index'; import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index'; import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast'; import type { Statement } from './query/ast';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MetonaSqlark // MetonaSqlark
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class MetonaSqlark { export class MetonaSqlark {
/** 数据库名称 */ /** 数据库名称 */
readonly name: string; readonly name: string;
/** 存储模式 */ /** 存储模式 */
readonly mode: string; readonly mode: string;
/** 版本号 */ /** 版本号 */
private _version: number; private _version: number;
/** 获取版本号 */ /** 获取版本号 */
get version(): number { return this._version; } get version(): number { return this._version; }
private engine!: IStorageEngine; private engine!: IStorageEngine;
private executor!: QueryExecutor; private executor!: QueryExecutor;
private transactionManager!: TransactionManager; private transactionManager!: TransactionManager;
private pluginManager: PluginManager; private pluginManager: PluginManager;
private config: DatabaseConfig; private config: DatabaseConfig;
private ready = false; private ready = false;
private tableCache: Map<string, Table> = new Map(); private tableCache: Map<string, Table> = new Map();
/** 查询结果行数上限 */ /** 查询结果行数上限 */
get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; } get maxRowsPerQuery(): number { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */ /** 调试模式 */
get debug(): boolean { return this.config.debug ?? false; } get debug(): boolean { return this.config.debug ?? false; }
constructor(config: DatabaseConfig) { /** 多标签页同步通道(v0.3.2 */
this.config = config; private channel: BroadcastChannel | null = null;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode; constructor(config: DatabaseConfig) {
this._version = config.version ?? DB_DEFAULTS.version; this.config = config;
this.pluginManager = new PluginManager(); 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 广播表变更
async init(): Promise<void> { if (config.multiTabSync && typeof BroadcastChannel !== 'undefined') {
// 创建引擎 this.channel = new BroadcastChannel(`metona-sqlark:${this.name}`);
this.engine = this.createEngine(); this.channel.onmessage = (event) => {
const msg = event.data as { type?: string; table?: string } | null;
// 打开连接 if (!msg || msg.type !== 'change') return;
await this.engine.open(this.name, this.version); this.emit(msg.table ?? '', { type: 'external', table: msg.table ?? '' });
// Hybrid 引擎:从磁盘重载内存,保证读到其他标签页的最新数据
// 初始化执行器和事务管理器 if (this.engine instanceof HybridEngine) {
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery); (this.engine as HybridEngine).reloadMemoryFromDisk().catch(() => {
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; /** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 回调 // 创建引擎
if (this.config.onReady) { this.engine = this.createEngine();
this.config.onReady(this);
} // 打开连接
} await this.engine.open(this.name, this.version);
/** 检查是否就绪 */ // 初始化执行器和事务管理器
isReady(): boolean { this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
return this.ready; this.transactionManager = new TransactionManager(this.engine);
}
// 注册插件
// ---- 表管理 ---- if (this.config.plugins) {
for (const plugin of this.config.plugins) {
/** 创建表 */ this.pluginManager.register(plugin, this);
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> { }
this.ensureReady(); }
const schema = createSchema(name, columns);
this.ready = true;
try {
await this.pluginManager.trigger('beforeCreateTable', schema); // 回调
await this.engine.createTable(schema); if (this.config.onReady) {
await this.pluginManager.trigger('afterCreateTable', schema); this.config.onReady(this);
} catch (error) { }
this._onError(error as Error); }
throw error;
} /** 检查是否就绪 */
isReady(): boolean {
// 清除缓存 return this.ready;
this.tableCache.delete(name); }
}
// ---- 表管理 ----
/** 获取表操作对象 */
table(name: string): Table { /** 创建表 */
this.ensureReady(); async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
let t = this.tableCache.get(name); const schema = createSchema(name, columns);
if (!t) {
t = new Table(this.engine, name, this.executor); try {
this.tableCache.set(name, t); await this.pluginManager.trigger('beforeCreateTable', schema);
} await this.engine.createTable(schema);
return t; await this.pluginManager.trigger('afterCreateTable', schema);
} } catch (error) {
this._onError(error as Error);
/** 删除表 */ throw error;
async dropTable(name: string): Promise<void> { }
this.ensureReady();
try { // 清除缓存
await this.pluginManager.trigger('beforeDropTable', name); this.tableCache.delete(name);
await this.engine.dropTable(name); }
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) { /** 获取表操作对象 */
this._onError(error as Error); table(name: string): Table {
throw error; this.ensureReady();
}
this.tableCache.delete(name); let t = this.tableCache.get(name);
} if (!t) {
// v0.3.2: 表操作写入后广播变更(多标签页同步)
/** 获取所有表名 */ t = new Table(this.engine, name, this.executor, (tableName) => this.broadcastChange(tableName));
async getTableNames(): Promise<string[]> { this.tableCache.set(name, t);
this.ensureReady(); }
return this.engine.getTableNames(); return t;
} }
// ---- SQL 查询 ---- /** 删除表 */
async dropTable(name: string): Promise<void> {
/** 执行 SQL 字符串查询 */ this.ensureReady();
async query(sql: string): Promise<unknown> { try {
this.ensureReady(); await this.pluginManager.trigger('beforeDropTable', name);
const startTime = this.debug ? Date.now() : 0; await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
await this.pluginManager.trigger('beforeQuery', sql); } catch (error) {
this._onError(error as Error);
let result: unknown; throw error;
try { }
const stmt: Statement = parse(sql); this.tableCache.delete(name);
result = await this.executor.execute(stmt); }
} catch (error) {
this._onError(error as Error); /** 获取所有表名 */
throw error; async getTableNames(): Promise<string[]> {
} this.ensureReady();
return this.engine.getTableNames();
await this.pluginManager.trigger('afterQuery', sql, result); }
if (this.debug) { // ---- SQL 查询 ----
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? (result as any[]).length : 0; /** 执行 SQL 字符串查询 */
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`); async query(sql: string): Promise<unknown> {
} this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
return result;
} await this.pluginManager.trigger('beforeQuery', sql);
// ---- 事务 ---- let result: unknown;
try {
/** 执行事务 */ // v0.3.0: 支持分号分隔的多语句,逐条顺序执行,返回最后一条的结果
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> { const statements: Statement[] = parseAll(sql);
this.ensureReady(); for (const stmt of statements) {
await this.pluginManager.trigger('beforeTransaction'); result = await this.executor.execute(stmt);
try { // v0.3.2: 写语句广播表变更(多标签页同步)
const result = await this.transactionManager.execute(fn); const table = this.writeStatementTable(stmt);
await this.pluginManager.trigger('afterTransaction'); if (table) this.broadcastChange(table);
return result; }
} catch (error) { } catch (error) {
this._onError(error as Error); this._onError(error as Error);
throw error; throw error;
} }
}
await this.pluginManager.trigger('afterQuery', sql, result);
// ---- 导入导出 ----
if (this.debug) {
/** 导出表数据为 JSON */ const elapsed = Date.now() - startTime;
async exportTable(tableName: string): Promise<Record<string, unknown>[]> { const rows = Array.isArray(result) ? (result as any[]).length : 0;
this.ensureReady(); this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
return this.engine.find(tableName, { table: tableName }); }
}
return result;
/** 导入 JSON 数据到表 */ }
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady(); // ---- 事务 ----
try {
return await this.engine.insert(tableName, data); /** 执行事务 */
} catch (error) { async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this._onError(error as Error); this.ensureReady();
throw error; await this.pluginManager.trigger('beforeTransaction');
} try {
} const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
/** 导出整个数据库为 JSON */ return result;
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> { } catch (error) {
this.ensureReady(); this._onError(error as Error);
const result: Record<string, Record<string, unknown>[]> = {}; throw error;
const names = await this.engine.getTableNames(); }
for (const name of names) { }
result[name] = await this.engine.find(name, { table: name });
} // ---- 导入导出 ----
return result;
} /** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
// ---- 发布订阅 ---- this.ensureReady();
return this.engine.find(tableName, { table: tableName });
private listeners: Map<string, Set<(data: unknown) => void>> = new Map(); }
/** 订阅表变更 */ /** 导入 JSON 数据到表 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown }) => void): () => void { async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
const key = `change:${tableName}`; this.ensureReady();
if (!this.listeners.has(key)) this.listeners.set(key, new Set()); try {
this.listeners.get(key)!.add(callback as (data: unknown) => void); return await this.engine.insert(tableName, data);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void); } catch (error) {
} this._onError(error as Error);
throw error;
/** 触发变更事件 */ }
emit(tableName: string, event: { type: string; row?: unknown }): void { }
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event)); /** 导出整个数据库为 JSON */
} async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
// ---- 迁移 ---- const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map(); for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
/** 注册迁移 */ }
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void { return result;
this.migrations.set(version, up); }
}
// ---- 发布订阅 ----
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> { private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) { /** 订阅表变更 */
if (version <= targetVersion && version > this.version) { subscribe(tableName: string, callback: (event: { type: string; row?: unknown; table?: string }) => void): () => void {
await up(this); const key = `change:${tableName}`;
this._version = version; 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}`;
getPluginManager(): PluginManager { this.listeners.get(key)?.forEach((cb) => cb(event));
return this.pluginManager; }
}
// ---- 多标签页同步(v0.3.2 ----
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void { /** 广播表变更到其他标签页(多标签页同步) */
this.pluginManager.on(hook, callback); broadcastChange(tableName: string): void {
} if (!this.channel) return;
try {
// ---- 生命周期 ---- this.channel.postMessage({ type: 'change', table: tableName });
} catch {
/** 关闭数据库 */ // 广播失败不影响主流程
async close(): Promise<void> { }
this.pluginManager.destroy(); }
await this.engine.close();
this.tableCache.clear(); /** 写语句对应的表名(多标签页广播用) */
this.ready = false; private writeStatementTable(stmt: Statement): string | null {
} switch (stmt.type) {
case 'INSERT': return stmt.into;
/** 获取底层引擎 */ case 'UPDATE': return stmt.table;
getEngine(): IStorageEngine { case 'DELETE': return stmt.from;
return this.engine; case 'CREATE_TABLE':
} case 'DROP_TABLE':
case 'TRUNCATE_TABLE':
// ---- 内部 ---- return stmt.name;
case 'ALTER_TABLE': return stmt.name;
private createEngine(): IStorageEngine { case 'CREATE_INDEX':
const mode = this.mode; case 'DROP_INDEX':
const diskEngine = this.config.diskEngine ?? 'indexeddb'; return stmt.table;
default:
switch (mode) { return null;
case 'memory': }
return new MemoryEngine(); }
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine(); // ---- 迁移 ----
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' }); private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
case 'hybrid':
return new HybridEngine(diskEngine); /** 注册迁移 */
default: addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR'); this.migrations.set(version, up);
} }
}
/** 执行迁移到指定版本 */
private ensureReady(): void { async migrateTo(targetVersion: number): Promise<void> {
if (!this.ready) { this.ensureReady();
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY'); 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;
/** 错误回调分发 */ }
private _onError(error: Error): void { }
if (this.config.onError) { }
try { this.config.onError(error); } catch { /* 避免回调自身异常影响主流程 */ }
} // ---- 插件 ----
}
/** 获取插件管理器 */
/** 调试日志 */ getPluginManager(): PluginManager {
private _debug(msg: string, ...args: unknown[]): void { return this.pluginManager;
if (this.debug) { }
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
} /** 注册钩子 */
} 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 * AriaEngine Buffer Pool Eviction LRU
* @module engine/aria/buffer/eviction * @module engine/aria/buffer/eviction
*/ */
import type { PageHandle } from '../types'; import type { PageHandle } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// LRU 双向链表 // LRU 双向链表
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* LRU most recently used least recently used * LRU most recently used least recently used
*/ */
export class LRUList { export class LRUList {
private head: PageHandle | null = null; private head: PageHandle | null = null;
private tail: PageHandle | null = null; private tail: PageHandle | null = null;
private _size = 0; private _size = 0;
get size(): number { get size(): number {
return this._size; return this._size;
} }
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */ /** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page: PageHandle): void { moveToHead(page: PageHandle): void {
// 如果已经在头部,无需操作 // 如果已经在头部,无需操作
if (this.head === page) return; if (this.head === page) return;
// 检测是否在链表中 // 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page; const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) { if (inList) {
// 先从当前位置移除 // 先从当前位置移除
this.detach(page); this.detach(page);
} else { } else {
this._size++; this._size++;
} }
// 插入头部 // 插入头部
page.prev = null; page.prev = null;
page.next = this.head; page.next = this.head;
if (this.head) { if (this.head) {
this.head.prev = page; this.head.prev = page;
} }
this.head = page; this.head = page;
if (!this.tail) { if (!this.tail) {
this.tail = page; this.tail = page;
} }
} }
/** 从链表中移除页面 */ /** 从链表中移除页面 */
remove(page: PageHandle): void { remove(page: PageHandle): void {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page; const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList) return; if (!inList) return;
this.detach(page); this.detach(page);
this._size = Math.max(0, this._size - 1); this._size = Math.max(0, this._size - 1);
} }
/** 内部:只调整指针,不修改 _size */ /** 内部:只调整指针,不修改 _size */
private detach(page: PageHandle): void { private detach(page: PageHandle): void {
if (page.prev) { if (page.prev) {
page.prev.next = page.next; page.prev.next = page.next;
} else if (this.head === page) { } else if (this.head === page) {
this.head = page.next; this.head = page.next;
} }
if (page.next) { if (page.next) {
page.next.prev = page.prev; page.next.prev = page.prev;
} else if (this.tail === page) { } else if (this.tail === page) {
this.tail = page.prev; this.tail = page.prev;
} }
page.prev = null; page.prev = null;
page.next = null; page.next = null;
} }
/** 获取 LRU 尾部(最久未使用的页面) */ /** 获取 LRU 尾部(最久未使用的页面) */
getLRU(): PageHandle | null { getLRU(): PageHandle | null {
return this.tail; return this.tail;
} }
/** 弹出 LRU 尾部 */ /** 弹出 LRU 尾部 */
popLRU(): PageHandle | null { popLRU(): PageHandle | null {
const lru = this.tail; const lru = this.tail;
if (lru) { if (lru) {
this.remove(lru); this.remove(lru);
} }
return lru; return lru;
} }
/** 清空链表 */ /** 清空链表 */
clear(): void { clear(): void {
this.head = null; this.head = null;
this.tail = null; this.tail = null;
this._size = 0; this._size = 0;
} }
/** 获取所有页面(用于迭代) */ /** 获取所有页面(用于迭代) */
getAllPages(): PageHandle[] { getAllPages(): PageHandle[] {
const pages: PageHandle[] = []; const pages: PageHandle[] = [];
let current = this.head; let current = this.head;
while (current) { while (current) {
pages.push(current); pages.push(current);
current = current.next; current = current.next;
} }
return pages; return pages;
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Eviction 策略 // Eviction 策略
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export type EvictionCallback = (page: PageHandle) => Promise<void>; export type EvictionCallback = (page: PageHandle) => Promise<void>;
/** /**
* Buffer Pool * Buffer Pool
*/ */
export class EvictionManager { export class EvictionManager {
private lru: LRUList; private lru: LRUList;
private onEvict: EvictionCallback; private onEvict: EvictionCallback;
private capacity: number; private capacity: number;
constructor(capacity: number, onEvict: EvictionCallback) { constructor(capacity: number, onEvict: EvictionCallback) {
this.lru = new LRUList(); this.lru = new LRUList();
this.capacity = capacity; this.capacity = capacity;
this.onEvict = onEvict; this.onEvict = onEvict;
} }
/** 访问页面,更新 LRU */ /** 访问页面,更新 LRU */
access(page: PageHandle): void { access(page: PageHandle): void {
page.lastAccess = Date.now(); page.lastAccess = Date.now();
this.lru.moveToHead(page); this.lru.moveToHead(page);
} }
/** 添加新页面到池中 */ /** 添加新页面到池中 */
add(page: PageHandle): void { add(page: PageHandle): void {
this.access(page); this.access(page);
} }
/** 移除指定页面 */ /** 移除指定页面 */
remove(page: PageHandle): void { remove(page: PageHandle): void {
this.lru.remove(page); this.lru.remove(page);
} }
/** /**
* *
* pin dirty=false * pin dirty=false
* *
*/ */
async evictIfNeeded(count: number): Promise<number> { async evictIfNeeded(count: number): Promise<number> {
let evicted = 0; let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) { while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面 // 找到可驱逐的页面
const victim = this.findEvictionCandidate(); const victim = this.findEvictionCandidate();
if (!victim) break; if (!victim) break;
// 脏页先刷盘 // 脏页先刷盘
if (victim.dirty) { if (victim.dirty) {
await this.onEvict(victim); await this.onEvict(victim);
victim.dirty = false; victim.dirty = false;
} }
this.lru.remove(victim); this.lru.remove(victim);
evicted++; evicted++;
} }
return evicted; return evicted;
} }
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */ /** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
private findEvictionCandidate(): PageHandle | null { private findEvictionCandidate(): PageHandle | null {
// 先从尾部找未 pin 的干净页面 // 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU(); let current = this.lru.getLRU();
while (current) { while (current) {
if (current.pins === 0 && !current.dirty) return current; if (current.pins === 0 && !current.dirty) return current;
current = current.prev; current = current.prev;
} }
// 没有干净页,找未 pin 的脏页 // 没有干净页,找未 pin 的脏页
current = this.lru.getLRU(); current = this.lru.getLRU();
while (current) { while (current) {
if (current.pins === 0) return current; if (current.pins === 0) return current;
current = current.prev; current = current.prev;
} }
return null; return null;
} }
/** 获取当前大小 */ /** 获取当前大小 */
getSize(): number { getSize(): number {
return this.lru.size; return this.lru.size;
} }
/** 获取容量 */ /** 获取容量 */
getCapacity(): number { getCapacity(): number {
return this.capacity; return this.capacity;
} }
/** 清空 */ /** 清空 */
clear(): void { clear(): void {
this.lru.clear(); this.lru.clear();
} }
} }
+185 -185
View File
@@ -1,185 +1,185 @@
/** /**
* AriaEngine Buffer Pool * AriaEngine Buffer Pool
* @module engine/aria/buffer/pool * @module engine/aria/buffer/pool
* *
* LRU 访 * LRU 访
*/ */
import type { PageHandle } from '../types'; import type { PageHandle } from '../types';
import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types'; import { PageType, DEFAULT_BUFFER_POOL_PAGES } from '../types';
import { createPage } from '../page/format'; import { createPage } from '../page/format';
import { EvictionManager } from './eviction'; import { EvictionManager } from './eviction';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Page Read / Write 回调 // Page Read / Write 回调
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface PageIO { export interface PageIO {
/** 从存储后端读取页面 */ /** 从存储后端读取页面 */
readPage(pageId: number): Promise<ArrayBuffer | null>; readPage(pageId: number): Promise<ArrayBuffer | null>;
/** 将页面写入存储后端 */ /** 将页面写入存储后端 */
writePage(pageId: number, data: ArrayBuffer): Promise<void>; writePage(pageId: number, data: ArrayBuffer): Promise<void>;
/** 分配新页面 ID */ /** 分配新页面 ID */
allocatePageId(): Promise<number>; allocatePageId(): Promise<number>;
/** 释放页面 ID */ /** 释放页面 ID */
freePageId(pageId: number): Promise<void>; freePageId(pageId: number): Promise<void>;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Buffer Pool // Buffer Pool
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class BufferPool { export class BufferPool {
private pages: Map<number, PageHandle> = new Map(); private pages: Map<number, PageHandle> = new Map();
private eviction: EvictionManager; private eviction: EvictionManager;
private pageIO: PageIO; private pageIO: PageIO;
private nextPageId = 0; private nextPageId = 0;
constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) { constructor(pageIO: PageIO, capacity: number = DEFAULT_BUFFER_POOL_PAGES) {
this.pageIO = pageIO; this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => { this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) { if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data); await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false; page.dirty = false;
} }
}); });
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 页面获取 // 页面获取
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
/** /**
* *
* pin 使 unpin() * pin 使 unpin()
*/ */
async getPage(pageId: number): Promise<PageHandle | null> { async getPage(pageId: number): Promise<PageHandle | null> {
// 已在池中 // 已在池中
let page = this.pages.get(pageId); let page = this.pages.get(pageId);
if (page) { if (page) {
this.eviction.access(page); this.eviction.access(page);
page.pins++; page.pins++;
return page; return page;
} }
// 需要从磁盘加载 // 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId); const buffer = await this.pageIO.readPage(pageId);
if (!buffer) return null; if (!buffer) return null;
// 确保有空间 // 确保有空间
await this.eviction.evictIfNeeded(1); await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4) as PageType; const type = new DataView(buffer).getUint8(4) as PageType;
page = { page = {
pageId, pageId,
type, type,
data: buffer, data: buffer,
dirty: false, dirty: false,
pins: 1, pins: 1,
prev: null, prev: null,
next: null, next: null,
lastAccess: Date.now(), lastAccess: Date.now(),
}; };
this.pages.set(pageId, page); this.pages.set(pageId, page);
this.eviction.add(page); this.eviction.add(page);
return page; return page;
} }
/** /**
* *
*/ */
async newPage(type: PageType = PageType.DATA): Promise<PageHandle> { async newPage(type: PageType = PageType.DATA): Promise<PageHandle> {
const pageId = await this.pageIO.allocatePageId(); const pageId = await this.pageIO.allocatePageId();
await this.eviction.evictIfNeeded(1); await this.eviction.evictIfNeeded(1);
const page = createPage(pageId, type); const page = createPage(pageId, type);
page.pins = 1; page.pins = 1;
this.pages.set(pageId, page); this.pages.set(pageId, page);
this.eviction.add(page); this.eviction.add(page);
return page; return page;
} }
/** /**
* pin * pin
*/ */
unpin(page: PageHandle): void { unpin(page: PageHandle): void {
if (page.pins > 0) { if (page.pins > 0) {
page.pins--; page.pins--;
} }
} }
/** /**
* *
*/ */
markDirty(page: PageHandle): void { markDirty(page: PageHandle): void {
page.dirty = true; page.dirty = true;
} }
/** /**
* *
*/ */
async flushPage(pageId: number): Promise<void> { async flushPage(pageId: number): Promise<void> {
const page = this.pages.get(pageId); const page = this.pages.get(pageId);
if (page && page.dirty) { if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data); await this.pageIO.writePage(pageId, page.data);
page.dirty = false; page.dirty = false;
} }
} }
/** /**
* *
*/ */
async flushAll(): Promise<void> { async flushAll(): Promise<void> {
for (const [, page] of this.pages) { for (const [, page] of this.pages) {
if (page.dirty) { if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data); await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false; page.dirty = false;
} }
} }
} }
/** /**
* *
*/ */
removePage(pageId: number): void { removePage(pageId: number): void {
const page = this.pages.get(pageId); const page = this.pages.get(pageId);
if (page) { if (page) {
this.eviction.remove(page); this.eviction.remove(page);
this.pages.delete(pageId); this.pages.delete(pageId);
} }
} }
/** /**
* *
*/ */
async clear(): Promise<void> { async clear(): Promise<void> {
await this.flushAll(); await this.flushAll();
this.pages.clear(); this.pages.clear();
this.eviction.clear(); this.eviction.clear();
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 统计 // 统计
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
/** 获取当前缓存页面数 */ /** 获取当前缓存页面数 */
getCachedPageCount(): number { getCachedPageCount(): number {
return this.pages.size; return this.pages.size;
} }
/** 获取缓存容量 */ /** 获取缓存容量 */
getCapacity(): number { getCapacity(): number {
return this.eviction.getCapacity(); return this.eviction.getCapacity();
} }
/** 获取脏页面数 */ /** 获取脏页面数 */
getDirtyPageCount(): number { getDirtyPageCount(): number {
let count = 0; let count = 0;
for (const [, page] of this.pages) { for (const [, page] of this.pages) {
if (page.dirty) count++; if (page.dirty) count++;
} }
return count; return count;
} }
} }
+39 -19
View File
@@ -2,39 +2,48 @@
* AriaEngine LZ4 Compression LZ4 / * AriaEngine LZ4 Compression LZ4 /
* @module engine/aria/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 : * Token 1 :
* hi 4bit = litLen (0-15) * 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] [litLen bytes] [2B LE offset]
* : [token with lo=0] [litLen bytes] * : [token with lo=0] [litLen bytes]
*/ */
const MIN_MATCH = 4; const MIN_MATCH = 4;
const MAX_MATCH = MIN_MATCH + 15; // 19,匹配长度上限
export function compressLZ4(input: Uint8Array): Uint8Array { 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); const out = new Uint8Array(maxOut);
let si = 0, di = 0; let si = 0, di = 0;
let litStart = 0; let litStart = 0;
while (si < input.byteLength) { while (si < input.byteLength) {
// 搜索最长 backward match // 搜索最长 backward match(截断到 MAX_MATCH,避免 token 字段溢出)
let bestLen = 0, bestOff = 0; let bestLen = 0, bestOff = 0;
const searchStart = Math.max(0, si - 65535); const searchStart = Math.max(0, si - 65535);
for (let p = searchStart; p < si; p++) { for (let p = searchStart; p < si; p++) {
let ml = 0; let ml = 0;
while (si + ml < input.byteLength && p + ml < si && 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 (ml >= MIN_MATCH && ml > bestLen) { bestLen = ml; bestOff = si - p; }
} }
if (bestLen >= MIN_MATCH && (si - litStart) <= 15) { // 仅当匹配完整可编码(field 1-15)且字面量不超过 15 时才输出组合 token
// 有匹配 → 输出组合 token(字面量+匹配) if (bestLen > MIN_MATCH && (si - litStart) <= 15) {
const litLen = si - litStart; 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); out[di++] = ((litLen & 0x0F) << 4) | (matchField & 0x0F);
for (let j = 0; j < litLen; j++) out[di++] = input[litStart + j]; for (let j = 0; j < litLen; j++) out[di++] = input[litStart + j];
out[di++] = bestOff & 0xFF; out[di++] = bestOff & 0xFF;
@@ -42,8 +51,15 @@ export function compressLZ4(input: Uint8Array): Uint8Array {
si += bestLen; si += bestLen;
litStart = si; litStart = si;
} else { } else {
// 无匹配或字面量已满 15 → 继续累积(不单独输出,等下个匹配合并) // 无匹配 / 匹配长度 4(field=0 有歧义)→ 继续累积字面量
si++; 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; 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 { export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Array {
@@ -74,16 +93,17 @@ export function decompressLZ4(input: Uint8Array, originalSize: number): Uint8Arr
out[di++] = input[si++]; 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 字节 // 组合 token:读取 offset + 复制匹配(可能自重叠
if (si + 1 < input.byteLength) { if (si + 1 >= input.byteLength) break;
const offset = input[si++] | (input[si++] << 8); const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH; const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) { for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset]; out[di] = out[di - offset];
di++; 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 * AriaEngine Bloom Filter key
* @module engine/aria/index/bloom * @module engine/aria/index/bloom
* *
* 使 + Kirsch-Mitzenmacher k * 使 + Kirsch-Mitzenmacher k
*/ */
import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types'; import { DEFAULT_BLOOM_BITS_PER_KEY } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// BloomFilter // BloomFilter
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class BloomFilter { export class BloomFilter {
private bits: Uint8Array; private bits: Uint8Array;
private numHashes: number; private numHashes: number;
private _inserted = 0; private _inserted = 0;
/** /**
* @param numKeys key * @param numKeys key
* @param bitsPerKey key 10 1% * @param bitsPerKey key 10 1%
*/ */
constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) { constructor(numKeys: number, bitsPerKey: number = DEFAULT_BLOOM_BITS_PER_KEY) {
// ceil(numKeys * bitsPerKey / 8),最少 64 位 // ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey); const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8); const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes); this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69 // k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69)); this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
} }
/** 从现有数据恢复 */ /** 从现有数据恢复 */
static fromData(data: Uint8Array, numHashes: number): BloomFilter { static fromData(data: Uint8Array, numHashes: number): BloomFilter {
const bf = new BloomFilter(1); // dummy const bf = new BloomFilter(1); // dummy
bf.bits = data; bf.bits = data;
bf.numHashes = numHashes; bf.numHashes = numHashes;
return bf; return bf;
} }
/** 插入 key */ /** 插入 key */
insert(key: string): void { insert(key: string): void {
const hashes = this.getHashes(key); const hashes = this.getHashes(key);
for (const h of hashes) { for (const h of hashes) {
const byteIdx = Math.floor(h / 8); const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8; const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx); this.bits[byteIdx] |= (1 << bitIdx);
} }
this._inserted++; this._inserted++;
} }
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */ /** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key: string): boolean { mayContain(key: string): boolean {
const hashes = this.getHashes(key); const hashes = this.getHashes(key);
for (const h of hashes) { for (const h of hashes) {
const byteIdx = Math.floor(h / 8); const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8; const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) { if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在 return false; // 确定不存在
} }
} }
return true; // 可能存在 return true; // 可能存在
} }
/** 获取序列化数据 */ /** 获取序列化数据 */
serialize(): Uint8Array { serialize(): Uint8Array {
return this.bits; return this.bits;
} }
/** bit 数组大小 */ /** bit 数组大小 */
getBitSize(): number { getBitSize(): number {
return this.bits.byteLength * 8; return this.bits.byteLength * 8;
} }
/** 已插入 key 数量 */ /** 已插入 key 数量 */
getInsertedCount(): number { getInsertedCount(): number {
return this._inserted; return this._inserted;
} }
/** hash 函数数量 */ /** hash 函数数量 */
getHashCount(): number { getHashCount(): number {
return this.numHashes; return this.numHashes;
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 哈希 // 哈希
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
private getHashes(key: string): number[] { private getHashes(key: string): number[] {
const bits = this.bits.byteLength * 8; const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key); const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key); const h2 = this.murmurSimple(key);
const hashes: number[] = []; const hashes: number[] = [];
for (let i = 0; i < this.numHashes; i++) { for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2 // Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits); const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h); hashes.push(h);
} }
return hashes; return hashes;
} }
/** FNV-1a 哈希 */ /** FNV-1a 哈希 */
private fnv1a(str: string): number { private fnv1a(str: string): number {
let hash = 0x811c9dc5; let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i); hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0; hash = (hash * 0x01000193) >>> 0;
} }
return hash; return hash;
} }
/** 简化的 Murmur-like 哈希 */ /** 简化的 Murmur-like 哈希 */
private murmurSimple(str: string): number { private murmurSimple(str: string): number {
let hash = 0; let hash = 0;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i); const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0; hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0; hash = (hash ^ (hash >>> 16)) >>> 0;
} }
return Math.abs(hash); return Math.abs(hash);
} }
} }
File diff suppressed because it is too large Load Diff
+467 -467
View File
@@ -1,467 +1,467 @@
/** /**
* AriaEngine MemTable * AriaEngine MemTable
* @module engine/aria/index/memtable * @module engine/aria/index/memtable
* *
* MemTable flush SSTable * MemTable flush SSTable
*/ */
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// RB-Tree Node // RB-Tree Node
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
enum Color { RED, BLACK } enum Color { RED, BLACK }
class RBNode<K, V> { class RBNode<K, V> {
key: K; key: K;
value: V; value: V;
color: Color = Color.RED; color: Color = Color.RED;
left: RBNode<K, V> | null = null; left: RBNode<K, V> | null = null;
right: RBNode<K, V> | null = null; right: RBNode<K, V> | null = null;
parent: RBNode<K, V> | null = null; parent: RBNode<K, V> | null = null;
constructor(key: K, value: V) { constructor(key: K, value: V) {
this.key = key; this.key = key;
this.value = value; this.value = value;
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Red-Black Tree // Red-Black Tree
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class RedBlackTree<K, V> { class RedBlackTree<K, V> {
private root: RBNode<K, V> | null = null; private root: RBNode<K, V> | null = null;
private _size = 0; private _size = 0;
get size(): number { return this._size; } get size(): number { return this._size; }
// ---- 插入 ---- // ---- 插入 ----
insert(key: K, value: V): void { insert(key: K, value: V): void {
const node = new RBNode(key, value); const node = new RBNode(key, value);
if (!this.root) { if (!this.root) {
this.root = node; this.root = node;
node.color = Color.BLACK; node.color = Color.BLACK;
this._size++; this._size++;
return; return;
} }
let parent: RBNode<K, V> | null = null; let parent: RBNode<K, V> | null = null;
let current: RBNode<K, V> | null = this.root; let current: RBNode<K, V> | null = this.root;
while (current) { while (current) {
parent = current; parent = current;
if (key < current.key) { if (key < current.key) {
current = current.left; current = current.left;
} else if (key > current.key) { } else if (key > current.key) {
current = current.right; current = current.right;
} else { } else {
// 更新已存在的 key // 更新已存在的 key
current.value = value; current.value = value;
return; return;
} }
} }
node.parent = parent; node.parent = parent;
if (key < parent!.key) { if (key < parent!.key) {
parent!.left = node; parent!.left = node;
} else { } else {
parent!.right = node; parent!.right = node;
} }
this._size++; this._size++;
this.fixInsert(node); this.fixInsert(node);
} }
// ---- 查找 ---- // ---- 查找 ----
find(key: K): V | null { find(key: K): V | null {
let current = this.root; let current = this.root;
while (current) { while (current) {
if (key < current.key) { if (key < current.key) {
current = current.left; current = current.left;
} else if (key > current.key) { } else if (key > current.key) {
current = current.right; current = current.right;
} else { } else {
return current.value; return current.value;
} }
} }
return null; return null;
} }
// ---- 删除 ---- // ---- 删除 ----
delete(key: K): boolean { delete(key: K): boolean {
// 简化实现:标记删除(实际改为找到并调整树) // 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key); const node = this.findNode(key);
if (!node) return false; if (!node) return false;
this.deleteNode(node); this.deleteNode(node);
this._size--; this._size--;
return true; return true;
} }
// ---- 遍历 ---- // ---- 遍历 ----
/** 中序遍历(有序) */ /** 中序遍历(有序) */
inorder(callback: (key: K, value: V) => void): void { inorder(callback: (key: K, value: V) => void): void {
this._inorder(this.root, callback); this._inorder(this.root, callback);
} }
/** 范围遍历 */ /** 范围遍历 */
rangeScan( rangeScan(
startKey: K, startKey: K,
endKey: K, endKey: K,
callback: (key: K, value: V) => void, callback: (key: K, value: V) => void,
): void { ): void {
this._rangeScan(this.root, startKey, endKey, callback); this._rangeScan(this.root, startKey, endKey, callback);
} }
/** 获取所有条目 */ /** 获取所有条目 */
getAllEntries(): [K, V][] { getAllEntries(): [K, V][] {
const entries: [K, V][] = []; const entries: [K, V][] = [];
this.inorder((k, v) => entries.push([k, v])); this.inorder((k, v) => entries.push([k, v]));
return entries; return entries;
} }
/** 清空 */ /** 清空 */
clear(): void { clear(): void {
this.root = null; this.root = null;
this._size = 0; this._size = 0;
} }
// ---- 内部方法 ---- // ---- 内部方法 ----
private findNode(key: K): RBNode<K, V> | null { private findNode(key: K): RBNode<K, V> | null {
let current = this.root; let current = this.root;
while (current) { while (current) {
if (key < current.key) { if (key < current.key) {
current = current.left; current = current.left;
} else if (key > current.key) { } else if (key > current.key) {
current = current.right; current = current.right;
} else { } else {
return current; return current;
} }
} }
return null; return null;
} }
private deleteNode(node: RBNode<K, V>): void { private deleteNode(node: RBNode<K, V>): void {
// 简化:用左子树最大或右子树最小替换 // 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略 // 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) { if (!node.left && !node.right) {
this.transplant(node, null); this.transplant(node, null);
if (node.color === Color.BLACK) this.fixDelete(null, node.parent); if (node.color === Color.BLACK) this.fixDelete(null, node.parent);
} else if (!node.left) { } else if (!node.left) {
this.transplant(node, node.right); this.transplant(node, node.right);
if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent); if (node.color === Color.BLACK) this.fixDelete(node.right, node.right!.parent);
} else if (!node.right) { } else if (!node.right) {
this.transplant(node, node.left); this.transplant(node, node.left);
if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent); if (node.color === Color.BLACK) this.fixDelete(node.left, node.left!.parent);
} else { } else {
const successor = this.minimum(node.right); const successor = this.minimum(node.right);
if (successor!.parent !== node) { if (successor!.parent !== node) {
this.transplant(successor!, successor!.right); this.transplant(successor!, successor!.right);
successor!.right = node.right; successor!.right = node.right;
successor!.right!.parent = successor; successor!.right!.parent = successor;
} }
this.transplant(node, successor); this.transplant(node, successor);
successor!.left = node.left; successor!.left = node.left;
successor!.left!.parent = successor; successor!.left!.parent = successor;
const origColor = successor!.color; const origColor = successor!.color;
successor!.color = node.color; successor!.color = node.color;
if (origColor === Color.BLACK) this.fixDelete(successor!.right, successor!.right?.parent ?? null); 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 { private transplant(u: RBNode<K, V> | null, v: RBNode<K, V> | null): void {
if (!u!.parent) { if (!u!.parent) {
this.root = v; this.root = v;
} else if (u === u!.parent.left) { } else if (u === u!.parent.left) {
u!.parent.left = v; u!.parent.left = v;
} else { } else {
u!.parent.right = v; u!.parent.right = v;
} }
if (v) v.parent = u!.parent; if (v) v.parent = u!.parent;
} }
private minimum(node: RBNode<K, V>): RBNode<K, V> { private minimum(node: RBNode<K, V>): RBNode<K, V> {
while (node.left) node = node.left; while (node.left) node = node.left;
return node; return node;
} }
private fixInsert(node: RBNode<K, V>): void { private fixInsert(node: RBNode<K, V>): void {
while (node.parent && node.parent.color === Color.RED) { while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent; const parent = node.parent;
const grandparent = parent.parent; const grandparent = parent.parent;
if (!grandparent) break; if (!grandparent) break;
if (parent === grandparent.left) { if (parent === grandparent.left) {
const uncle = grandparent.right; const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) { if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK; parent.color = Color.BLACK;
uncle.color = Color.BLACK; uncle.color = Color.BLACK;
grandparent.color = Color.RED; grandparent.color = Color.RED;
node = grandparent; node = grandparent;
} else { } else {
if (node === parent.right) { if (node === parent.right) {
node = parent; node = parent;
this.rotateLeft(node); this.rotateLeft(node);
} }
if (node.parent) node.parent.color = Color.BLACK; if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED; if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateRight(node.parent.parent); if (node.parent?.parent) this.rotateRight(node.parent.parent);
} }
} else { } else {
const uncle = grandparent.left; const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) { if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK; parent.color = Color.BLACK;
uncle.color = Color.BLACK; uncle.color = Color.BLACK;
grandparent.color = Color.RED; grandparent.color = Color.RED;
node = grandparent; node = grandparent;
} else { } else {
if (node === parent.left) { if (node === parent.left) {
node = parent; node = parent;
this.rotateRight(node); this.rotateRight(node);
} }
if (node.parent) node.parent.color = Color.BLACK; if (node.parent) node.parent.color = Color.BLACK;
if (node.parent?.parent) node.parent.parent.color = Color.RED; if (node.parent?.parent) node.parent.parent.color = Color.RED;
if (node.parent?.parent) this.rotateLeft(node.parent.parent); if (node.parent?.parent) this.rotateLeft(node.parent.parent);
} }
} }
} }
if (this.root) this.root.color = Color.BLACK; if (this.root) this.root.color = Color.BLACK;
} }
private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void { private fixDelete(x: RBNode<K, V> | null, parent: RBNode<K, V> | null): void {
// 标准 RB-Tree 删除修复(修复"双黑"问题) // 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x; let node = x;
let nodeParent = parent; let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) { while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent) break; if (!nodeParent) break;
if (node === nodeParent.left) { if (node === nodeParent.left) {
let sibling = nodeParent.right; let sibling = nodeParent.right;
if (!sibling) break; if (!sibling) break;
// Case 1: 兄弟是红色 // Case 1: 兄弟是红色
if (sibling.color === Color.RED) { if (sibling.color === Color.RED) {
sibling.color = Color.BLACK; sibling.color = Color.BLACK;
nodeParent.color = Color.RED; nodeParent.color = Color.RED;
this.rotateLeft(nodeParent); this.rotateLeft(nodeParent);
sibling = nodeParent.right; sibling = nodeParent.right;
if (!sibling) break; if (!sibling) break;
} }
// Case 2: 兄弟的两个子节点都是黑色 // Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left; const sibLeft = sibling.left;
const sibRight = sibling.right; const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) && if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) { (!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED; sibling.color = Color.RED;
node = nodeParent; node = nodeParent;
nodeParent = node.parent; nodeParent = node.parent;
} else { } else {
// Case 3: 兄弟右子黑色(左子红色) // Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) { if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft) sibLeft.color = Color.BLACK; if (sibLeft) sibLeft.color = Color.BLACK;
sibling.color = Color.RED; sibling.color = Color.RED;
this.rotateRight(sibling); this.rotateRight(sibling);
sibling = nodeParent.right; sibling = nodeParent.right;
if (!sibling) break; if (!sibling) break;
} }
// Case 4: 兄弟右子红色 // Case 4: 兄弟右子红色
sibling.color = nodeParent.color; sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK; nodeParent.color = Color.BLACK;
if (sibling.right) sibling.right.color = Color.BLACK; if (sibling.right) sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent); this.rotateLeft(nodeParent);
node = this.root; node = this.root;
} }
} else { } else {
// 镜像:node 是父节点的右子 // 镜像:node 是父节点的右子
let sibling = nodeParent.left; let sibling = nodeParent.left;
if (!sibling) break; if (!sibling) break;
if (sibling.color === Color.RED) { if (sibling.color === Color.RED) {
sibling.color = Color.BLACK; sibling.color = Color.BLACK;
nodeParent.color = Color.RED; nodeParent.color = Color.RED;
this.rotateRight(nodeParent); this.rotateRight(nodeParent);
sibling = nodeParent.left; sibling = nodeParent.left;
if (!sibling) break; if (!sibling) break;
} }
const sibLeft = sibling.left; const sibLeft = sibling.left;
const sibRight = sibling.right; const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) && if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) { (!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED; sibling.color = Color.RED;
node = nodeParent; node = nodeParent;
nodeParent = node.parent; nodeParent = node.parent;
} else { } else {
if (!sibLeft || sibLeft.color === Color.BLACK) { if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight) sibRight.color = Color.BLACK; if (sibRight) sibRight.color = Color.BLACK;
sibling.color = Color.RED; sibling.color = Color.RED;
this.rotateLeft(sibling); this.rotateLeft(sibling);
sibling = nodeParent.left; sibling = nodeParent.left;
if (!sibling) break; if (!sibling) break;
} }
sibling.color = nodeParent.color; sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK; nodeParent.color = Color.BLACK;
if (sibling.left) sibling.left.color = Color.BLACK; if (sibling.left) sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent); this.rotateRight(nodeParent);
node = this.root; node = this.root;
} }
} }
} }
if (node) node.color = Color.BLACK; if (node) node.color = Color.BLACK;
} }
private rotateLeft(x: RBNode<K, V>): void { private rotateLeft(x: RBNode<K, V>): void {
const y = x.right; const y = x.right;
if (!y) return; if (!y) return;
x.right = y.left; x.right = y.left;
if (y.left) y.left.parent = x; if (y.left) y.left.parent = x;
y.parent = x.parent; y.parent = x.parent;
if (!x.parent) { if (!x.parent) {
this.root = y; this.root = y;
} else if (x === x.parent.left) { } else if (x === x.parent.left) {
x.parent.left = y; x.parent.left = y;
} else { } else {
x.parent.right = y; x.parent.right = y;
} }
y.left = x; y.left = x;
x.parent = y; x.parent = y;
} }
private rotateRight(x: RBNode<K, V>): void { private rotateRight(x: RBNode<K, V>): void {
const y = x.left; const y = x.left;
if (!y) return; if (!y) return;
x.left = y.right; x.left = y.right;
if (y.right) y.right.parent = x; if (y.right) y.right.parent = x;
y.parent = x.parent; y.parent = x.parent;
if (!x.parent) { if (!x.parent) {
this.root = y; this.root = y;
} else if (x === x.parent.right) { } else if (x === x.parent.right) {
x.parent.right = y; x.parent.right = y;
} else { } else {
x.parent.left = y; x.parent.left = y;
} }
y.right = x; y.right = x;
x.parent = y; x.parent = y;
} }
private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void { private _inorder(node: RBNode<K, V> | null, cb: (k: K, v: V) => void): void {
if (!node) return; if (!node) return;
this._inorder(node.left, cb); this._inorder(node.left, cb);
cb(node.key, node.value); cb(node.key, node.value);
this._inorder(node.right, cb); this._inorder(node.right, cb);
} }
private _rangeScan( private _rangeScan(
node: RBNode<K, V> | null, node: RBNode<K, V> | null,
start: K, start: K,
end: K, end: K,
cb: (k: K, v: V) => void, cb: (k: K, v: V) => void,
): void { ): void {
if (!node) return; if (!node) return;
if (node.key > start) this._rangeScan(node.left, start, end, cb); 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 >= start && node.key <= end) cb(node.key, node.value);
if (node.key < end) this._rangeScan(node.right, start, end, cb); if (node.key < end) this._rangeScan(node.right, start, end, cb);
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MemTable // MemTable
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class MemTable { export class MemTable {
private tree: RedBlackTree<string, Record<string, unknown>>; private tree: RedBlackTree<string, Record<string, unknown>>;
private _estimatedSize = 0; private _estimatedSize = 0;
private maxSize: number; private maxSize: number;
constructor(maxSize: number = 4 * 1024 * 1024) { constructor(maxSize: number = 4 * 1024 * 1024) {
this.tree = new RedBlackTree(); this.tree = new RedBlackTree();
this.maxSize = maxSize; this.maxSize = maxSize;
} }
/** 插入或更新 */ /** 插入或更新 */
put(key: string, value: Record<string, unknown>): void { put(key: string, value: Record<string, unknown>): void {
const oldSize = this.estimateEntrySize(key, this.tree.find(key)); const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value); const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value); this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize; this._estimatedSize += newSize - oldSize;
} }
/** 获取 */ /** 获取 */
get(key: string): Record<string, unknown> | null { get(key: string): Record<string, unknown> | null {
return this.tree.find(key); return this.tree.find(key);
} }
/** 删除 */ /** 删除 */
delete(key: string): boolean { delete(key: string): boolean {
const oldVal = this.tree.find(key); const oldVal = this.tree.find(key);
if (oldVal) { if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal); this._estimatedSize -= this.estimateEntrySize(key, oldVal);
} }
return this.tree.delete(key); return this.tree.delete(key);
} }
/** 是否应刷盘 */ /** 是否应刷盘 */
shouldFlush(): boolean { shouldFlush(): boolean {
return this._estimatedSize >= this.maxSize; return this._estimatedSize >= this.maxSize;
} }
/** 获取所有有序条目 */ /** 获取所有有序条目 */
getAllEntries(): [string, Record<string, unknown>][] { getAllEntries(): [string, Record<string, unknown>][] {
return this.tree.getAllEntries(); return this.tree.getAllEntries();
} }
/** 范围扫描 */ /** 范围扫描 */
rangeScan( rangeScan(
startKey: string, startKey: string,
endKey: string, endKey: string,
): [string, Record<string, unknown>][] { ): [string, Record<string, unknown>][] {
const entries: [string, Record<string, unknown>][] = []; const entries: [string, Record<string, unknown>][] = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v])); this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries; return entries;
} }
/** 条目数 */ /** 条目数 */
getEntryCount(): number { getEntryCount(): number {
return this.tree.size; return this.tree.size;
} }
/** 估计大小(字节) */ /** 估计大小(字节) */
getEstimatedSize(): number { getEstimatedSize(): number {
return this._estimatedSize; return this._estimatedSize;
} }
/** 清空 */ /** 清空 */
clear(): void { clear(): void {
this.tree.clear(); this.tree.clear();
this._estimatedSize = 0; this._estimatedSize = 0;
} }
/** 检查 key 是否存在 */ /** 检查 key 是否存在 */
contains(key: string): boolean { contains(key: string): boolean {
return this.tree.find(key) !== null; return this.tree.find(key) !== null;
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 内部 // 内部
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
private estimateEntrySize(key: string, value: Record<string, unknown> | null): number { private estimateEntrySize(key: string, value: Record<string, unknown> | null): number {
if (!value) return 0; if (!value) return 0;
let size = key.length * 2; // UTF-16 let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) { for (const entry of Object.entries(value)) {
size += entry[0].length * 2; size += entry[0].length * 2;
const v = entry[1]; const v = entry[1];
if (typeof v === 'string') size += v.length * 2; if (typeof v === 'string') size += v.length * 2;
else if (typeof v === 'number') size += 8; else if (typeof v === 'number') size += 8;
else if (typeof v === 'boolean') size += 1; else if (typeof v === 'boolean') size += 1;
else if (v === null || v === undefined) size += 1; else if (v === null || v === undefined) size += 1;
else size += 16; // rough estimate else size += 16; // rough estimate
} }
return size; return size;
} }
} }
+192 -189
View File
@@ -1,189 +1,192 @@
/** /**
* AriaEngine Merge Iterator * AriaEngine Merge Iterator
* @module engine/aria/index/merge_iterator * @module engine/aria/index/merge_iterator
* *
* SSTable MemTable * SSTable MemTable
*/ */
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MergeIterator // MergeIterator
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface EntrySource { export interface EntrySource {
/** 获取下一个条目,无更多时返回 null */ /** 获取下一个条目,无更多时返回 null */
next(): [string, Record<string, unknown>] | null; next(): [string, Record<string, unknown>] | null;
/** 重置迭代器 */ /** 重置迭代器 */
reset(): void; reset(): void;
} }
/** 数组数据源的迭代器 */ /** 数组数据源的迭代器 */
export class ArrayEntrySource implements EntrySource { export class ArrayEntrySource implements EntrySource {
private entries: [string, Record<string, unknown>][]; private entries: [string, Record<string, unknown>][];
private index = 0; private index = 0;
constructor(entries: [string, Record<string, unknown>][]) { constructor(entries: [string, Record<string, unknown>][]) {
this.entries = entries; this.entries = entries;
} }
next(): [string, Record<string, unknown>] | null { next(): [string, Record<string, unknown>] | null {
if (this.index >= this.entries.length) return null; if (this.index >= this.entries.length) return null;
return this.entries[this.index++]; return this.entries[this.index++];
} }
reset(): void { reset(): void {
this.index = 0; this.index = 0;
} }
} }
/** 回调数据源的迭代器 */ /** 回调数据源的迭代器 */
export class CallbackEntrySource implements EntrySource { export class CallbackEntrySource implements EntrySource {
private items: [string, Record<string, unknown>][] = []; private items: [string, Record<string, unknown>][] = [];
private index = 0; private index = 0;
private consumed = false; private consumed = false;
/** /**
* @param producer * @param producer
*/ */
constructor(producer: (cb: (key: string, value: Record<string, unknown>) => void) => void) { constructor(producer: (cb: (key: string, value: Record<string, unknown>) => void) => void) {
producer((key, value) => this.items.push([key, value])); producer((key, value) => this.items.push([key, value]));
} }
next(): [string, Record<string, unknown>] | null { next(): [string, Record<string, unknown>] | null {
if (this.index >= this.items.length) return null; if (this.index >= this.items.length) return null;
return this.items[this.index++]; return this.items[this.index++];
} }
reset(): void { reset(): void {
this.index = 0; this.index = 0;
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Heap 节点(用于多路归并) // Heap 节点(用于多路归并)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
interface HeapNode { interface HeapNode {
key: string; key: string;
value: Record<string, unknown>; value: Record<string, unknown>;
sourceIndex: number; sourceIndex: number;
} }
/** 最小堆 */ /** 最小堆 */
class MinHeap { class MinHeap {
private heap: HeapNode[] = []; private heap: HeapNode[] = [];
push(node: HeapNode): void { push(node: HeapNode): void {
this.heap.push(node); this.heap.push(node);
this.bubbleUp(this.heap.length - 1); this.bubbleUp(this.heap.length - 1);
} }
pop(): HeapNode | null { pop(): HeapNode | null {
if (this.heap.length === 0) return null; if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop()!; if (this.heap.length === 1) return this.heap.pop()!;
const result = this.heap[0]; const result = this.heap[0];
this.heap[0] = this.heap.pop()!; this.heap[0] = this.heap.pop()!;
this.bubbleDown(0); this.bubbleDown(0);
return result; return result;
} }
peek(): HeapNode | null { peek(): HeapNode | null {
return this.heap.length > 0 ? this.heap[0] : null; return this.heap.length > 0 ? this.heap[0] : null;
} }
get size(): number { get size(): number {
return this.heap.length; return this.heap.length;
} }
private bubbleUp(idx: number): void { private bubbleUp(idx: number): void {
while (idx > 0) { while (idx > 0) {
const parent = Math.floor((idx - 1) / 2); const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key) break; if (this.heap[idx].key >= this.heap[parent].key) break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]]; [this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent; idx = parent;
} }
} }
private bubbleDown(idx: number): void { private bubbleDown(idx: number): void {
const n = this.heap.length; const n = this.heap.length;
while (true) { while (true) {
let smallest = idx; let smallest = idx;
const left = 2 * idx + 1; const left = 2 * idx + 1;
const right = 2 * idx + 2; const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key) smallest = left; 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 (right < n && this.heap[right].key < this.heap[smallest].key) smallest = right;
if (smallest === idx) break; if (smallest === idx) break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]]; [this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest; idx = smallest;
} }
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MergeIterator // MergeIterator
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* key * key
* MemTable SSTable * MemTable SSTable
*/ */
export class MergeIterator { export class MergeIterator {
private sources: EntrySource[]; private sources: EntrySource[];
private heap: MinHeap; private heap: MinHeap;
constructor() { constructor() {
this.sources = []; this.sources = [];
this.heap = new MinHeap(); this.heap = new MinHeap();
} }
/** 添加数据源 */ /** 添加数据源 */
addSource(source: EntrySource): void { addSource(source: EntrySource): void {
this.sources.push(source); this.sources.push(source);
this.seedFromSource(this.sources.length - 1); this.seedFromSource(this.sources.length - 1);
} }
/** 获取下一个归并后的条目 */ /** 获取下一个归并后的条目 */
next(): [string, Record<string, unknown>] | null { next(): [string, Record<string, unknown>] | null {
if (this.heap.size === 0) return null; if (this.heap.size === 0) return null;
const node = this.heap.pop()!; const first = this.heap.pop()!;
const key = node.key; const key = first.key;
const value = node.value; let best = first;
// 刷新来源的下一个值 // 刷新 first 来源的下一个值
this.seedFromSource(node.sourceIndex); this.seedFromSource(first.sourceIndex);
// 跳过重复 key取最新的(堆顶的即是最新的,因为来源下标越小越新) // 跳过重复 key在多个来源中保留 sourceIndex 最小(最新)的条目
while (this.heap.peek() && this.heap.peek()!.key === key) { while (this.heap.peek() && this.heap.peek()!.key === key) {
const dup = this.heap.pop()!; const dup = this.heap.pop()!;
this.seedFromSource(dup.sourceIndex); this.seedFromSource(dup.sourceIndex);
} if (dup.sourceIndex < best.sourceIndex) {
best = dup;
return [key, value]; }
} }
/** 耗尽管道,返回所有归并结果 */ return [best.key, best.value];
drain(): [string, Record<string, unknown>][] { }
const result: [string, Record<string, unknown>][] = [];
let entry = this.next(); /** 耗尽管道,返回所有归并结果 */
while (entry) { drain(): [string, Record<string, unknown>][] {
result.push(entry); const result: [string, Record<string, unknown>][] = [];
entry = this.next(); let entry = this.next();
} while (entry) {
return result; result.push(entry);
} entry = this.next();
}
private seedFromSource(sourceIndex: number): void { return result;
const entry = this.sources[sourceIndex].next(); }
if (entry) {
this.heap.push({ private seedFromSource(sourceIndex: number): void {
key: entry[0], const entry = this.sources[sourceIndex].next();
value: entry[1], if (entry) {
sourceIndex, this.heap.push({
}); key: entry[0],
} value: entry[1],
} sourceIndex,
} });
}
}
}
+261 -261
View File
@@ -1,261 +1,261 @@
/** /**
* AriaEngine SSTable Reader SSTable * AriaEngine SSTable Reader SSTable
* @module engine/aria/index/sstable * @module engine/aria/index/sstable
*/ */
import type { IndexEntry, SSTableMeta } from '../types'; import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom'; import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB" const SSTABLE_MAGIC = 0x53535442; // "SSTB"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// SSTableReader // SSTableReader
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class SSTableReader { export class SSTableReader {
private data: Uint8Array; private data: Uint8Array;
private view: DataView; private view: DataView;
private indexEntries: IndexEntry[] = []; private indexEntries: IndexEntry[] = [];
private entryCount = 0; private entryCount = 0;
private meta: SSTableMeta; private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null; private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) { constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data; this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta; this.meta = meta;
this.parseFooter(); this.parseFooter();
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 查询 // 查询
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
/** 精确查找 key */ /** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null { get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定 // Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null; if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey); const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null; if (blockIdx < 0) return null;
const entry = this.indexEntries[blockIdx]; const entry = this.indexEntries[blockIdx];
const blockData = new Uint8Array( const blockData = new Uint8Array(
this.data.buffer, this.data.buffer,
this.data.byteOffset + entry.blockOffset, this.data.byteOffset + entry.blockOffset,
entry.blockSize, entry.blockSize,
); );
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength); const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const entryCount = blockView.getUint32(0, false); const entryCount = blockView.getUint32(0, false);
let offset = 4; let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找) // 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) { for (let i = 0; i < entryCount; i++) {
const keyLen = blockView.getUint16(offset, false); const keyLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen)); const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen; offset += keyLen;
const valLen = blockView.getUint16(offset, false); const valLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const valBytes = blockData.slice(offset, offset + valLen); const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen; offset += valLen;
if (key === targetKey) { if (key === targetKey) {
try { try {
return JSON.parse(new TextDecoder().decode(valBytes)); return JSON.parse(new TextDecoder().decode(valBytes));
} catch { } catch {
return null; return null;
} }
} }
} }
return null; return null;
} }
/** 范围扫描 */ /** 范围扫描 */
rangeScan( rangeScan(
startKey: string, startKey: string,
endKey: string, endKey: string,
callback: (key: string, value: Record<string, unknown>) => void, callback: (key: string, value: Record<string, unknown>) => void,
): void { ): void {
if (this.indexEntries.length === 0) return; if (this.indexEntries.length === 0) return;
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey)); const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey)); const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return; if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx) return;
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) { for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi]; const entry = this.indexEntries[bi];
const blockData = new Uint8Array( const blockData = new Uint8Array(
this.data.buffer, this.data.buffer,
this.data.byteOffset + entry.blockOffset, this.data.byteOffset + entry.blockOffset,
entry.blockSize, entry.blockSize,
); );
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength); const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false); const blockEntryCount = blockView.getUint32(0, false);
let offset = 4; let offset = 4;
for (let i = 0; i < blockEntryCount; i++) { for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false); const keyLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen)); const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen; offset += keyLen;
const valLen = blockView.getUint16(offset, false); const valLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const valBytes = blockData.slice(offset, offset + valLen); const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen; offset += valLen;
if (key >= startKey && key <= endKey) { if (key >= startKey && key <= endKey) {
try { try {
const value = JSON.parse(new TextDecoder().decode(valBytes)); const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value); callback(key, value);
} catch { } catch {
// skip corrupted entry // skip corrupted entry
} }
} }
} }
} }
} }
/** 扫描所有条目 */ /** 扫描所有条目 */
scanAll(callback: (key: string, value: Record<string, unknown>) => void): void { scanAll(callback: (key: string, value: Record<string, unknown>) => void): void {
for (const entry of this.indexEntries) { for (const entry of this.indexEntries) {
const blockData = new Uint8Array( const blockData = new Uint8Array(
this.data.buffer, this.data.buffer,
this.data.byteOffset + entry.blockOffset, this.data.byteOffset + entry.blockOffset,
entry.blockSize, entry.blockSize,
); );
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength); const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false); const blockEntryCount = blockView.getUint32(0, false);
let offset = 4; let offset = 4;
for (let i = 0; i < blockEntryCount; i++) { for (let i = 0; i < blockEntryCount; i++) {
const keyLen = blockView.getUint16(offset, false); const keyLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen)); const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen; offset += keyLen;
const valLen = blockView.getUint16(offset, false); const valLen = blockView.getUint16(offset, false);
offset += 2; offset += 2;
const valBytes = blockData.slice(offset, offset + valLen); const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen; offset += valLen;
try { try {
const value = JSON.parse(new TextDecoder().decode(valBytes)); const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value); callback(key, value);
} catch { } catch {
// skip corrupted entry // skip corrupted entry
} }
} }
} }
} }
/** 获取元数据 */ /** 获取元数据 */
getMeta(): SSTableMeta { getMeta(): SSTableMeta {
return this.meta; return this.meta;
} }
/** 获取索引条目数 */ /** 获取索引条目数 */
getIndexBlockCount(): number { getIndexBlockCount(): number {
return this.indexEntries.length; return this.indexEntries.length;
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 内部 // 内部
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
private parseFooter(): void { private parseFooter(): void {
if (this.data.byteLength < 32) { if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer'); throw new Error('SSTable too small: missing footer');
} }
const footerOffset = this.data.byteLength - 32; const footerOffset = this.data.byteLength - 32;
// 验证魔数 // 验证魔数
const magic = this.view.getUint32(footerOffset + 24, false); const magic = this.view.getUint32(footerOffset + 24, false);
if (magic !== SSTABLE_MAGIC) { if (magic !== SSTABLE_MAGIC) {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`); throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
} }
const indexOffset = this.view.getUint32(footerOffset, false); const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false); const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false); const bloomOffset = this.view.getUint32(footerOffset + 8, false);
const bloomSize = this.view.getUint32(footerOffset + 12, false); const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false); const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false); this.entryCount = this.view.getUint32(footerOffset + 20, false);
// 解析索引块 // 解析索引块
this.parseIndexBlock(indexOffset, indexSize); this.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter // 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) { if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try { try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize); const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10); this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch { } catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化) // 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
} }
} }
} }
private parseIndexBlock(offset: number, _size: number): void { private parseIndexBlock(offset: number, _size: number): void {
const entryCount = this.view.getUint32(offset, false); const entryCount = this.view.getUint32(offset, false);
offset += 4; offset += 4;
for (let i = 0; i < entryCount; i++) { for (let i = 0; i < entryCount; i++) {
const keyLen = this.view.getUint16(offset, false); const keyLen = this.view.getUint16(offset, false);
offset += 2; offset += 2;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen)); const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen; offset += keyLen;
const blockOffset = this.view.getUint32(offset, false); const blockOffset = this.view.getUint32(offset, false);
offset += 4; offset += 4;
const blockSize = this.view.getUint32(offset, false); const blockSize = this.view.getUint32(offset, false);
offset += 4; offset += 4;
this.indexEntries.push({ key, blockOffset, blockSize }); this.indexEntries.push({ key, blockOffset, blockSize });
} }
} }
/** 二分查找某 key 所在的 block 索引 */ /** 二分查找某 key 所在的 block 索引 */
private locateBlock(key: string): number { private locateBlock(key: string): number {
let lo = 0; let lo = 0;
let hi = this.indexEntries.length - 1; let hi = this.indexEntries.length - 1;
while (lo <= hi) { while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2); const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid]; const entry = this.indexEntries[mid];
if (key <= entry.key) { if (key <= entry.key) {
// 检查是否在此 block 范围内 // 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key; const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey) return mid; if (key > firstKey) return mid;
hi = mid - 1; hi = mid - 1;
} else { } else {
lo = mid + 1; lo = mid + 1;
} }
} }
return -1; return -1;
} }
private locateBlockGE(key: string): number { private locateBlockGE(key: string): number {
let lo = 0, hi = this.indexEntries.length; let lo = 0, hi = this.indexEntries.length;
while (lo < hi) { while (lo < hi) {
const mid = (lo + hi) >> 1; const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1; if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid; else hi = mid;
} }
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1; return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
} }
private locateBlockLE(key: string): number { private locateBlockLE(key: string): number {
let lo = 0, hi = this.indexEntries.length; let lo = 0, hi = this.indexEntries.length;
while (lo < hi) { while (lo < hi) {
const mid = (lo + hi) >> 1; const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1; if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid; else hi = mid;
} }
return lo > 0 ? lo - 1 : 0; return lo > 0 ? lo - 1 : 0;
} }
} }
+247 -249
View File
@@ -1,249 +1,247 @@
/** /**
* AriaEngine SSTable Builder * AriaEngine SSTable Builder
* @module engine/aria/index/sstable_builder * @module engine/aria/index/sstable_builder
* *
* key-value SSTable * key-value SSTable
* *
* SSTable : * SSTable :
* *
* Data Block 0 * Data Block 0
* Data Block 1 * Data Block 1
* ... * ...
* Index Block (block offset key range) * Index Block (block offset key range)
* Bloom Filter * Bloom Filter
* Footer (32 bytes) * Footer (32 bytes)
* - index_offset (u32) * - index_offset (u32)
* - index_size (u32) * - index_size (u32)
* - bloom_offset (u32) * - bloom_offset (u32)
* - bloom_size (u32) * - bloom_size (u32)
* - bloom_hash_count (u32) * - bloom_hash_count (u32)
* - entry_count (u32) * - entry_count (u32)
* - magic_number (u32, 0x53535442 ="SSTB") * - magic_number (u32, 0x53535442 ="SSTB")
* - checksum (u32) * - checksum (u32)
* *
*/ */
import { BloomFilter } from './bloom'; import { BloomFilter } from './bloom';
import type { IndexEntry, DataBlock } from '../types'; import type { IndexEntry } from '../types';
const SSTABLE_MAGIC = 0x53535442; // "SSTB" const SSTABLE_MAGIC = 0x53535442; // "SSTB"
const SSTABLE_FOOTER_SIZE = 32; const SSTABLE_FOOTER_SIZE = 32;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// SSTableBuilder // SSTableBuilder
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class SSTableBuilder { export class SSTableBuilder {
private entries: [string, Record<string, unknown>][] = []; private entries: [string, Record<string, unknown>][] = [];
private currentBlock: [string, Record<string, unknown>][] = []; private currentBlock: [string, Record<string, unknown>][] = [];
private currentBlockStartKey = ''; private currentBlockStartKey = '';
private blockSizeLimit: number; private blockSizeLimit: number;
constructor(blockSizeLimit: number = 4096) { constructor(blockSizeLimit: number = 4096) {
this.blockSizeLimit = blockSizeLimit; this.blockSizeLimit = blockSizeLimit;
} }
/** 添加一个 key-value 条目(必须按键排序添加) */ /** 添加一个 key-value 条目(必须按键排序添加) */
add(key: string, value: Record<string, unknown>): void { add(key: string, value: Record<string, unknown>): void {
if (this.currentBlock.length === 0) { if (this.currentBlock.length === 0) {
this.currentBlockStartKey = key; this.currentBlockStartKey = key;
} }
this.currentBlock.push([key, value]); this.currentBlock.push([key, value]);
this.entries.push([key, value]); this.entries.push([key, value]);
// 如果当前 Block 达到大小限制,切割 // 如果当前 Block 达到大小限制,切割
const estimated = this.estimateBlockSize(); const estimated = this.estimateBlockSize();
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) { if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) {
// 当前 block 结束(不在这里切割,在 build 时统一处理) // 当前 block 结束(不在这里切割,在 build 时统一处理)
} }
} }
/** /**
* SSTable * SSTable
* { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter } * { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
*/ */
build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } { build(): { sstableData: Uint8Array; indexEntries: IndexEntry[] } {
const blocks = this.splitIntoBlocks(); const blocks = this.splitIntoBlocks();
const bloomFilter = new BloomFilter(this.entries.length); const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小 // 预计算总大小
let totalSize = 0; let totalSize = 0;
const blockOffsets: number[] = []; const blockOffsets: number[] = [];
for (const block of blocks) { for (const block of blocks) {
blockOffsets.push(totalSize); blockOffsets.push(totalSize);
const blockSize = this.computeBlockSize(block); const blockSize = this.computeBlockSize(block);
totalSize += blockSize; totalSize += blockSize;
} }
// 索引块 // 索引块
const indexEntries: IndexEntry[] = []; const indexEntries: IndexEntry[] = [];
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
const block = blocks[i]; const block = blocks[i];
const lastKey = block[block.length - 1][0]; const lastKey = block[block.length - 1][0];
const blockSize = this.computeBlockSize(block); const blockSize = this.computeBlockSize(block);
indexEntries.push({ indexEntries.push({
key: lastKey, key: lastKey,
blockOffset: blockOffsets[i], blockOffset: blockOffsets[i],
blockSize, blockSize,
}); });
} }
const indexBlockSize = this.estimateIndexBlockSize(indexEntries); const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小 // 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize(); const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength; const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block // 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE; const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize); const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf); const view = new DataView(buf);
let offset = 0; let offset = 0;
// ---- Data Blocks ---- // ---- Data Blocks ----
for (const block of blocks) { for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter); offset = this.writeDataBlock(view, offset, block, bloomFilter);
} }
// ---- Index Block ---- // ---- Index Block ----
const indexOffset = offset; const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries); offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ---- // ---- Bloom Filter Block ----
const bloomOffset = offset; const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset); new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize; offset += bloomSize;
// ---- Footer ---- // ---- Footer ----
const footerOffset = offset; const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false); view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false); view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false); view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0) view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return { return {
sstableData: new Uint8Array(buf), sstableData: new Uint8Array(buf),
indexEntries, indexEntries,
}; };
} }
/** 获取条目数 */ /** 获取条目数 */
getEntryCount(): number { getEntryCount(): number {
return this.entries.length; return this.entries.length;
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 内部 // 内部
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
private splitIntoBlocks(): [string, Record<string, unknown>][][] { private splitIntoBlocks(): [string, Record<string, unknown>][][] {
const blocks: [string, Record<string, unknown>][][] = []; const blocks: [string, Record<string, unknown>][][] = [];
let current: [string, Record<string, unknown>][] = []; let current: [string, Record<string, unknown>][] = [];
for (const entry of this.entries) { for (const entry of this.entries) {
current.push(entry); current.push(entry);
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) { if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1)); blocks.push(current.slice(0, -1));
current = [entry]; current = [entry];
} }
} }
if (current.length > 0) blocks.push(current); if (current.length > 0) blocks.push(current);
return blocks; return blocks;
} }
private estimateBlockSize(): number { private estimateBlockSize(): number {
return this.estimateBlockSizeFromEntries(this.currentBlock); return this.estimateBlockSizeFromEntries(this.currentBlock);
} }
private estimateBlockSizeFromEntries(entries: [string, unknown][]): number { private estimateBlockSizeFromEntries(entries: [string, unknown][]): number {
let size = 0; let size = 0;
for (const [key, value] of entries) { for (const [key, value] of entries) {
size += 4 + key.length + JSON.stringify(value).length; size += 4 + key.length + JSON.stringify(value).length;
} }
return size; return size;
} }
private computeBlockSize(block: [string, unknown][]): number { private computeBlockSize(block: [string, unknown][]): number {
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json // entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
let size = 4; let size = 4;
for (const [key, value] of block) { for (const [key, value] of block) {
const json = JSON.stringify(value); const json = JSON.stringify(value);
size += 2 + key.length + 2 + json.length; size += 2 + key.length + 2 + json.length;
} }
return size; return size;
} }
private writeDataBlock( private writeDataBlock(
view: DataView, view: DataView,
offset: number, offset: number,
block: [string, Record<string, unknown>][], block: [string, Record<string, unknown>][],
bloomFilter: BloomFilter, bloomFilter: BloomFilter,
): number { ): number {
const start = offset; // entry count
view.setUint32(offset, block.length, false);
// entry count offset += 4;
view.setUint32(offset, block.length, false);
offset += 4; for (const [key, value] of block) {
const encoder = new TextEncoder();
for (const [key, value] of block) { const keyBytes = encoder.encode(key);
const encoder = new TextEncoder(); const valueBytes = encoder.encode(JSON.stringify(value));
const keyBytes = encoder.encode(key);
const valueBytes = encoder.encode(JSON.stringify(value)); // key length
view.setUint16(offset, keyBytes.length, false);
// key length offset += 2;
view.setUint16(offset, keyBytes.length, false); // key
offset += 2; new Uint8Array(view.buffer).set(keyBytes, offset);
// key offset += keyBytes.length;
new Uint8Array(view.buffer).set(keyBytes, offset); // value length
offset += keyBytes.length; view.setUint16(offset, valueBytes.length, false);
// value length offset += 2;
view.setUint16(offset, valueBytes.length, false); // value
offset += 2; new Uint8Array(view.buffer).set(valueBytes, offset);
// value offset += valueBytes.length;
new Uint8Array(view.buffer).set(valueBytes, offset);
offset += valueBytes.length; // 插入 bloom filter
bloomFilter.insert(key);
// 插入 bloom filter }
bloomFilter.insert(key);
} return offset;
}
return offset;
} private estimateIndexBlockSize(entries: IndexEntry[]): number {
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
private estimateIndexBlockSize(entries: IndexEntry[]): number { let size = 4;
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32) for (const entry of entries) {
let size = 4; size += 2 + entry.key.length + 8;
for (const entry of entries) { }
size += 2 + entry.key.length + 8; return size;
} }
return size;
} private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number {
view.setUint32(offset, entries.length, false);
private writeIndexBlock(view: DataView, offset: number, entries: IndexEntry[]): number { offset += 4;
view.setUint32(offset, entries.length, false);
offset += 4; for (const entry of entries) {
const encoder = new TextEncoder();
for (const entry of entries) { const keyBytes = encoder.encode(entry.key);
const encoder = new TextEncoder(); view.setUint16(offset, keyBytes.length, false);
const keyBytes = encoder.encode(entry.key); offset += 2;
view.setUint16(offset, keyBytes.length, false); new Uint8Array(view.buffer).set(keyBytes, offset);
offset += 2; offset += keyBytes.length;
new Uint8Array(view.buffer).set(keyBytes, offset); view.setUint32(offset, entry.blockOffset, false);
offset += keyBytes.length; offset += 4;
view.setUint32(offset, entry.blockOffset, false); view.setUint32(offset, entry.blockSize, false);
offset += 4; offset += 4;
view.setUint32(offset, entry.blockSize, false); }
offset += 4;
} return offset;
}
return offset; }
}
}
+164 -167
View File
@@ -1,167 +1,164 @@
/** /**
* AriaEngine Page Format * AriaEngine Page Format
* @module engine/aria/page/format * @module engine/aria/page/format
* *
* Header / Slot / Tuple * Header / Slot / Tuple
*/ */
import { PAGE_SIZE, PageType, type PageHandle } from '../types'; import { PAGE_SIZE, PageType, type PageHandle } from '../types';
import { import {
initPageHeader, initPageHeader,
decodePageHeader, getSlotCount,
encodePageHeader, } from './header';
getSlotCount, import { allocateSlot, freeSlot, readSlotData } from './slot';
getPageId, import { encodeTuple, decodeTuple } from './tuple';
} 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);
export function createPage(pageId: number, type: PageType): PageHandle { return {
const data = new ArrayBuffer(PAGE_SIZE); pageId,
initPageHeader(data, pageId, type); type,
return { data,
pageId, dirty: true,
type, pins: 0,
data, prev: null,
dirty: true, next: null,
pins: 0, lastAccess: Date.now(),
prev: null, };
next: null, }
lastAccess: Date.now(),
}; /** 从 ArrayBuffer 恢复页面句柄 */
} export function pageFromBuffer(
pageId: number,
/** 从 ArrayBuffer 恢复页面句柄 */ buffer: ArrayBuffer,
export function pageFromBuffer( ): PageHandle {
pageId: number, return {
buffer: ArrayBuffer, pageId,
): PageHandle { type: new DataView(buffer).getUint8(4) as PageType,
return { data: buffer,
pageId, dirty: false,
type: new DataView(buffer).getUint8(4) as PageType, pins: 0,
data: buffer, prev: null,
dirty: false, next: null,
pins: 0, lastAccess: Date.now(),
prev: null, };
next: null, }
lastAccess: Date.now(),
}; // ---------------------------------------------------------------------------
} // 行操作(页面级)
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// 行操作(页面级) /**
// --------------------------------------------------------------------------- * slot -1
*/
/** export function pageInsertRow(
* slot -1 page: PageHandle,
*/ row: Record<string, unknown>,
export function pageInsertRow( columnOrder: string[],
page: PageHandle, columnTypes: Record<string, string>,
row: Record<string, unknown>, ): number {
columnOrder: string[], const encoded = encodeTuple(row, columnOrder, columnTypes);
columnTypes: Record<string, string>, const slotIdx = allocateSlot(page.data, encoded);
): number { if (slotIdx >= 0) {
const encoded = encodeTuple(row, columnOrder, columnTypes); page.dirty = true;
const slotIdx = allocateSlot(page.data, encoded); page.lastAccess = Date.now();
if (slotIdx >= 0) { }
page.dirty = true; return slotIdx;
page.lastAccess = Date.now(); }
}
return slotIdx; /**
} * slot
*/
/** export function pageReadRow(
* slot page: PageHandle,
*/ slotIndex: number,
export function pageReadRow( columnOrder: string[],
page: PageHandle, columnTypes: Record<string, string>,
slotIndex: number, ): Record<string, unknown> | null {
columnOrder: string[], const slotData = readSlotData(page.data, slotIndex);
columnTypes: Record<string, string>, if (!slotData) return null;
): Record<string, unknown> | null { return decodeTuple(slotData, columnOrder, columnTypes);
const slotData = readSlotData(page.data, slotIndex); }
if (!slotData) return null;
return decodeTuple(slotData, columnOrder, columnTypes); /**
} *
*/
/** export function pageReadAllRows(
* page: PageHandle,
*/ columnOrder: string[],
export function pageReadAllRows( columnTypes: Record<string, string>,
page: PageHandle, ): Record<string, unknown>[] {
columnOrder: string[], const rows: Record<string, unknown>[] = [];
columnTypes: Record<string, string>, const slotCount = getSlotCount(page.data);
): Record<string, unknown>[] { for (let i = 0; i < slotCount; i++) {
const rows: Record<string, unknown>[] = []; const row = pageReadRow(page, i, columnOrder, columnTypes);
const slotCount = getSlotCount(page.data); if (row) rows.push(row);
for (let i = 0; i < slotCount; i++) { }
const row = pageReadRow(page, i, columnOrder, columnTypes); return rows;
if (row) rows.push(row); }
}
return rows; /**
} * slot
*/
/** export function pageDeleteRow(page: PageHandle, slotIndex: number): void {
* slot freeSlot(page.data, slotIndex);
*/ page.dirty = true;
export function pageDeleteRow(page: PageHandle, slotIndex: number): void { page.lastAccess = Date.now();
freeSlot(page.data, slotIndex); }
page.dirty = true;
page.lastAccess = Date.now(); /**
} * slot
*/
/** export function pageUpdateRow(
* slot page: PageHandle,
*/ slotIndex: number,
export function pageUpdateRow( row: Record<string, unknown>,
page: PageHandle, columnOrder: string[],
slotIndex: number, columnTypes: Record<string, string>,
row: Record<string, unknown>, ): void {
columnOrder: string[], // 先标记旧 slot 为删除
columnTypes: Record<string, string>, pageDeleteRow(page, slotIndex);
): void { // 分配新 slot,可能会在不同位置
// 先标记旧 slot 为删除 pageInsertRow(page, row, columnOrder, columnTypes);
pageDeleteRow(page, slotIndex); // 注意:调用者需要自行维护 slot index → pk 的映射
// 分配新 slot,可能会在不同位置 }
const newSlot = pageInsertRow(page, row, columnOrder, columnTypes);
// 注意:调用者需要自行维护 slot index → pk 的映射 // ---------------------------------------------------------------------------
} // 校验和
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// 校验和 /** 简单 CRC32(使用预先计算的查找表简化) */
// --------------------------------------------------------------------------- export function computeChecksum(data: ArrayBuffer): number {
const view = new Uint8Array(data);
/** 简单 CRC32(使用预先计算的查找表简化) */ let hash = 0;
export function computeChecksum(data: ArrayBuffer): number { for (let i = 0; i < view.byteLength; i++) {
const view = new Uint8Array(data); hash = ((hash << 5) - hash + view[i]) | 0;
let hash = 0; }
for (let i = 0; i < view.byteLength; i++) { return hash >>> 0;
hash = ((hash << 5) - hash + view[i]) | 0; }
}
return hash >>> 0; /** 更新页面的校验和字段 */
} export function updateChecksum(page: PageHandle): void {
// 先清零校验和字段
/** 更新页面的校验和字段 */ const view = new DataView(page.data);
export function updateChecksum(page: PageHandle): void { view.setUint32(11, 0, false);
// 先清零校验和字段 // 计算校验和
const view = new DataView(page.data); const cksum = computeChecksum(page.data);
view.setUint32(11, 0, false); view.setUint32(11, cksum, 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);
/** 验证页面校验和 */ // 临时清零
export function verifyChecksum(page: PageHandle): boolean { new DataView(page.data).setUint32(11, 0, false);
const stored = new DataView(page.data).getUint32(11, false); const computed = computeChecksum(page.data);
// 临时清零 new DataView(page.data).setUint32(11, stored, false);
new DataView(page.data).setUint32(11, 0, false); return stored === computed;
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 * AriaEngine Page Header
* @module engine/aria/page/header * @module engine/aria/page/header
*/ */
import { PAGE_HEADER_SIZE, PageType, type PageHeader } from '../types'; import { PAGE_HEADER_SIZE, PageType, type PageHeader } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 编码 // 编码
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* PageHeader ArrayBuffer 16 * PageHeader ArrayBuffer 16
* : * :
* [0-3] page_id u32 * [0-3] page_id u32
* [4] type u8 * [4] type u8
* [5-6] free_start u16 * [5-6] free_start u16
* [7-8] free_end u16 * [7-8] free_end u16
* [9-10] slot_count u16 * [9-10] slot_count u16
* [11-14] checksum u32 * [11-14] checksum u32
* [15] reserved u8 * [15] reserved u8
*/ */
export function encodePageHeader(header: PageHeader, buf: ArrayBuffer): void { export function encodePageHeader(header: PageHeader, buf: ArrayBuffer): void {
const view = new DataView(buf); const view = new DataView(buf);
view.setUint32(0, header.pageId, false); view.setUint32(0, header.pageId, false);
view.setUint8(4, header.type); view.setUint8(4, header.type);
view.setUint16(5, header.freeStart, false); view.setUint16(5, header.freeStart, false);
view.setUint16(7, header.freeEnd, false); view.setUint16(7, header.freeEnd, false);
view.setUint16(9, header.slotCount, false); view.setUint16(9, header.slotCount, false);
view.setUint32(11, header.checksum, false); view.setUint32(11, header.checksum, false);
view.setUint8(15, 0); // reserved view.setUint8(15, 0); // reserved
} }
/** /**
* ArrayBuffer PageHeader * ArrayBuffer PageHeader
*/ */
export function decodePageHeader(buf: ArrayBuffer): PageHeader { export function decodePageHeader(buf: ArrayBuffer): PageHeader {
const view = new DataView(buf); const view = new DataView(buf);
return { return {
pageId: view.getUint32(0, false), pageId: view.getUint32(0, false),
type: view.getUint8(4) as PageType, type: view.getUint8(4) as PageType,
freeStart: view.getUint16(5, false), freeStart: view.getUint16(5, false),
freeEnd: view.getUint16(7, false), freeEnd: view.getUint16(7, false),
slotCount: view.getUint16(9, false), slotCount: view.getUint16(9, false),
checksum: view.getUint32(11, false), checksum: view.getUint32(11, false),
}; };
} }
/** /**
* Header * Header
*/ */
export function initPageHeader( export function initPageHeader(
buf: ArrayBuffer, buf: ArrayBuffer,
pageId: number, pageId: number,
type: PageType, type: PageType,
): void { ): void {
const view = new DataView(buf); const view = new DataView(buf);
view.setUint32(0, pageId, false); view.setUint32(0, pageId, false);
view.setUint8(4, type); view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后 view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾 view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0 view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0 view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0); view.setUint8(15, 0);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 从 Buffer 中提取 Header 字段的辅助函数 // 从 Buffer 中提取 Header 字段的辅助函数
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function getPageType(buf: ArrayBuffer): PageType { export function getPageType(buf: ArrayBuffer): PageType {
return new DataView(buf).getUint8(4) as PageType; return new DataView(buf).getUint8(4) as PageType;
} }
export function getPageId(buf: ArrayBuffer): number { export function getPageId(buf: ArrayBuffer): number {
return new DataView(buf).getUint32(0, false); return new DataView(buf).getUint32(0, false);
} }
export function getSlotCount(buf: ArrayBuffer): number { export function getSlotCount(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(9, false); return new DataView(buf).getUint16(9, false);
} }
export function getFreeStart(buf: ArrayBuffer): number { export function getFreeStart(buf: ArrayBuffer): number {
return new DataView(buf).getUint16(5, false); return new DataView(buf).getUint16(5, false);
} }
export function setFreeStart(buf: ArrayBuffer, val: number): void { export function setFreeStart(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(5, val, false); new DataView(buf).setUint16(5, val, false);
} }
export function setFreeEnd(buf: ArrayBuffer, val: number): void { export function setFreeEnd(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(7, val, false); new DataView(buf).setUint16(7, val, false);
} }
export function setSlotCount(buf: ArrayBuffer, val: number): void { export function setSlotCount(buf: ArrayBuffer, val: number): void {
new DataView(buf).setUint16(9, val, false); new DataView(buf).setUint16(9, val, false);
} }
+184 -184
View File
@@ -1,184 +1,184 @@
/** /**
* AriaEngine Slot Directory * AriaEngine Slot Directory
* @module engine/aria/page/slot * @module engine/aria/page/slot
* *
* Slot Header 4 * Slot Header 4
*/ */
import { import {
PAGE_HEADER_SIZE, PAGE_HEADER_SIZE,
SLOT_ENTRY_SIZE, SLOT_ENTRY_SIZE,
PAGE_SIZE, PAGE_SIZE,
type SlotEntry, type SlotEntry,
} from '../types'; } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Slot 读写 // Slot 读写
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* slot SlotEntry * slot SlotEntry
*/ */
export function getSlotEntry(buf: ArrayBuffer, slotIndex: number): SlotEntry { export function getSlotEntry(buf: ArrayBuffer, slotIndex: number): SlotEntry {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE; const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf); const view = new DataView(buf);
return { return {
offset: view.getUint16(offset, false), offset: view.getUint16(offset, false),
length: view.getUint16(offset + 2, false), length: view.getUint16(offset + 2, false),
}; };
} }
/** /**
* slot SlotEntry * slot SlotEntry
*/ */
export function setSlotEntry( export function setSlotEntry(
buf: ArrayBuffer, buf: ArrayBuffer,
slotIndex: number, slotIndex: number,
entry: SlotEntry, entry: SlotEntry,
): void { ): void {
const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE; const offset = PAGE_HEADER_SIZE + slotIndex * SLOT_ENTRY_SIZE;
const view = new DataView(buf); const view = new DataView(buf);
view.setUint16(offset, entry.offset, false); view.setUint16(offset, entry.offset, false);
view.setUint16(offset + 2, entry.length, false); view.setUint16(offset + 2, entry.length, false);
} }
/** /**
* slot * slot
*/ */
export function getAllSlots( export function getAllSlots(
buf: ArrayBuffer, buf: ArrayBuffer,
slotCount: number, slotCount: number,
): SlotEntry[] { ): SlotEntry[] {
const entries: SlotEntry[] = []; const entries: SlotEntry[] = [];
for (let i = 0; i < slotCount; i++) { for (let i = 0; i < slotCount; i++) {
entries.push(getSlotEntry(buf, i)); entries.push(getSlotEntry(buf, i));
} }
return entries; return entries;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Slot 空间计算 // Slot 空间计算
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** 获取 slot 目录占用的总字节数 */ /** 获取 slot 目录占用的总字节数 */
export function getSlotDirectorySize(slotCount: number): number { export function getSlotDirectorySize(slotCount: number): number {
return slotCount * SLOT_ENTRY_SIZE; return slotCount * SLOT_ENTRY_SIZE;
} }
/** 获取可用空闲空间(字节) */ /** 获取可用空闲空间(字节) */
export function getFreeSpace(buf: ArrayBuffer): number { export function getFreeSpace(buf: ArrayBuffer): number {
const view = new DataView(buf); const view = new DataView(buf);
const freeStart = view.getUint16(5, false); // slot 区之后 const freeStart = view.getUint16(5, false); // slot 区之后
const freeEnd = view.getUint16(7, false); // 数据区之前 const freeEnd = view.getUint16(7, false); // 数据区之前
return freeEnd - freeStart; return freeEnd - freeStart;
} }
/** 检查是否有足够空间存放长度为 len 的行 */ /** 检查是否有足够空间存放长度为 len 的行 */
export function hasEnoughSpace(buf: ArrayBuffer, len: number): boolean { export function hasEnoughSpace(buf: ArrayBuffer, len: number): boolean {
const slotCount = new DataView(buf).getUint16(9, false); const slotCount = new DataView(buf).getUint16(9, false);
const neededSlotSize = (slotCount + 1) * SLOT_ENTRY_SIZE; const neededSlotSize = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSize; const freeStart = PAGE_HEADER_SIZE + neededSlotSize;
const freeEnd = new DataView(buf).getUint16(7, false); const freeEnd = new DataView(buf).getUint16(7, false);
return freeEnd - freeStart >= len; return freeEnd - freeStart >= len;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 插入 / 删除 slot // 插入 / 删除 slot
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* slot * slot
* slot -1 * slot -1
*/ */
export function allocateSlot( export function allocateSlot(
buf: ArrayBuffer, buf: ArrayBuffer,
rowData: Uint8Array, rowData: Uint8Array,
): number { ): number {
const view = new DataView(buf); const view = new DataView(buf);
const slotCount = view.getUint16(9, false); const slotCount = view.getUint16(9, false);
const neededSlotSpace = (slotCount + 1) * SLOT_ENTRY_SIZE; const neededSlotSpace = (slotCount + 1) * SLOT_ENTRY_SIZE;
const freeStart = PAGE_HEADER_SIZE + neededSlotSpace; const freeStart = PAGE_HEADER_SIZE + neededSlotSpace;
const freeEnd = view.getUint16(7, false); const freeEnd = view.getUint16(7, false);
if (freeEnd - freeStart < rowData.byteLength) { if (freeEnd - freeStart < rowData.byteLength) {
return -1; // 空间不足 return -1; // 空间不足
} }
// 将数据放入页面底部 // 将数据放入页面底部
const dataOffset = freeEnd - rowData.byteLength; const dataOffset = freeEnd - rowData.byteLength;
const dest = new Uint8Array(buf, dataOffset, rowData.byteLength); const dest = new Uint8Array(buf, dataOffset, rowData.byteLength);
dest.set(rowData); dest.set(rowData);
// 写入 slot 条目 // 写入 slot 条目
setSlotEntry(buf, slotCount, { offset: dataOffset, length: rowData.byteLength }); setSlotEntry(buf, slotCount, { offset: dataOffset, length: rowData.byteLength });
// 更新 header // 更新 header
view.setUint16(5, freeStart, false); // freeStart view.setUint16(5, freeStart, false); // freeStart
view.setUint16(7, dataOffset, false); // freeEnd view.setUint16(7, dataOffset, false); // freeEnd
view.setUint16(9, slotCount + 1, false); // slotCount view.setUint16(9, slotCount + 1, false); // slotCount
return slotCount; return slotCount;
} }
/** /**
* slot offset 0 * slot offset 0
* slot * slot
*/ */
export function freeSlot(buf: ArrayBuffer, slotIndex: number): void { export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 }); setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
} }
/** /**
* slot * slot
* *
*/ */
export function compactSlots(buf: ArrayBuffer): number { export function compactSlots(buf: ArrayBuffer): number {
const view = new DataView(buf); const view = new DataView(buf);
const slotCount = view.getUint16(9, false); const slotCount = view.getUint16(9, false);
if (slotCount === 0) return 0; if (slotCount === 0) return 0;
// 收集有效 slotoffset>0 的) // 收集有效 slotoffset>0 的)
const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = []; const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = [];
for (let i = 0; i < slotCount; i++) { for (let i = 0; i < slotCount; i++) {
const entry = getSlotEntry(buf, i); const entry = getSlotEntry(buf, i);
if (entry.offset > 0 && entry.length > 0) { if (entry.offset > 0 && entry.length > 0) {
const data = new Uint8Array(buf, entry.offset, entry.length); const data = new Uint8Array(buf, entry.offset, entry.length);
validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) }); validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) });
} }
} }
if (validSlots.length === slotCount) return 0; // 无碎片 if (validSlots.length === slotCount) return 0; // 无碎片
// 从页面底部重新紧凑排列 // 从页面底部重新紧凑排列
let dataEnd = PAGE_SIZE; let dataEnd = PAGE_SIZE;
const newSlots: { offset: number; length: number }[] = []; const newSlots: { offset: number; length: number }[] = [];
for (let i = validSlots.length - 1; i >= 0; i--) { for (let i = validSlots.length - 1; i >= 0; i--) {
const s = validSlots[i]; const s = validSlots[i];
dataEnd -= s.length; dataEnd -= s.length;
new Uint8Array(buf).set(s.data, dataEnd); new Uint8Array(buf).set(s.data, dataEnd);
newSlots.unshift({ offset: dataEnd, length: s.length }); newSlots.unshift({ offset: dataEnd, length: s.length });
} }
// 重写 slot directory // 重写 slot directory
view.setUint16(9, validSlots.length, false); // slotCount view.setUint16(9, validSlots.length, false); // slotCount
view.setUint16(7, dataEnd, false); // freeEnd view.setUint16(7, dataEnd, false); // freeEnd
for (let i = 0; i < validSlots.length; i++) { for (let i = 0; i < validSlots.length; i++) {
setSlotEntry(buf, i, newSlots[i]); setSlotEntry(buf, i, newSlots[i]);
} }
// 清除剩余 slot 条目 // 清除剩余 slot 条目
for (let i = validSlots.length; i < slotCount; i++) { for (let i = validSlots.length; i < slotCount; i++) {
setSlotEntry(buf, i, { offset: 0, length: 0 }); setSlotEntry(buf, i, { offset: 0, length: 0 });
} }
return slotCount - validSlots.length; // 回收的 slot 数量 return slotCount - validSlots.length; // 回收的 slot 数量
} }
/** /**
* slot * slot
*/ */
export function readSlotData(buf: ArrayBuffer, slotIndex: number): Uint8Array | null { export function readSlotData(buf: ArrayBuffer, slotIndex: number): Uint8Array | null {
const entry = getSlotEntry(buf, slotIndex); const entry = getSlotEntry(buf, slotIndex);
if (entry.offset === 0 || entry.length === 0) return null; if (entry.offset === 0 || entry.length === 0) return null;
return new Uint8Array(buf, entry.offset, entry.length); return new Uint8Array(buf, entry.offset, entry.length);
} }
+251 -252
View File
@@ -1,252 +1,251 @@
/** /**
* AriaEngine Tuple Codec * AriaEngine Tuple Codec
* @module engine/aria/page/tuple * @module engine/aria/page/tuple
* *
* Record<string, unknown> * Record<string, unknown>
* *
* : * :
* [null bitmap: ceil(colCount/8) bytes] * [null bitmap: ceil(colCount/8) bytes]
* [column 1 data] * [column 1 data]
* [column 2 data] * [column 2 data]
* ... * ...
* *
* : * :
* type tag (u8) + data * type tag (u8) + data
* - STRING: [len: u16][UTF-8 bytes] * - STRING: [len: u16][UTF-8 bytes]
* - NUMBER: [f64: 8 bytes] * - NUMBER: [f64: 8 bytes]
* - BOOLEAN: [u8: 1 byte] * - BOOLEAN: [u8: 1 byte]
* - DATE: [f64: 8 bytes] (epoch ms) * - DATE: [f64: 8 bytes] (epoch ms)
* - JSON: [len: u16][UTF-8 bytes] * - JSON: [len: u16][UTF-8 bytes]
* - NULL: (no data, just the tag) * - NULL: (no data, just the tag)
*/ */
import { ColumnEncoding } from '../types'; import { ColumnEncoding } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 编码 // 编码
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* *
* @param row * @param row
* @param columnOrder * @param columnOrder
* @param columnTypes FieldType * @param columnTypes FieldType
*/ */
export function encodeTuple( export function encodeTuple(
row: Record<string, unknown>, row: Record<string, unknown>,
columnOrder: string[], columnOrder: string[],
columnTypes: Record<string, string>, columnTypes: Record<string, string>,
): Uint8Array { ): Uint8Array {
// 先计算总大小 // 先计算总大小
let size = 0; let size = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8); const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
size += nullBitmapBytes; size += nullBitmapBytes;
// 预计算每列编码后的字节 // 预计算每列编码后的字节
const colData: (Uint8Array | null)[] = []; const colData: (Uint8Array | null)[] = [];
for (let i = 0; i < columnOrder.length; i++) { for (let i = 0; i < columnOrder.length; i++) {
const col = columnOrder[i]; const col = columnOrder[i];
const val = row[col]; const val = row[col];
const encoded = encodeColumn(val, columnTypes[col] ?? 'string'); const encoded = encodeColumn(val, columnTypes[col] ?? 'string');
colData.push(encoded); colData.push(encoded);
if (encoded) { if (encoded) {
size += 1 + encoded.byteLength; // tag + data size += 1 + encoded.byteLength; // tag + data
} else { } else {
size += 1; // just the NULL tag size += 1; // just the NULL tag
} }
} }
const buf = new Uint8Array(size); const buf = new Uint8Array(size);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let offset = 0; let offset = 0;
// Null bitmap // Null bitmap
const nullBitmap = new Uint8Array(nullBitmapBytes); const nullBitmap = new Uint8Array(nullBitmapBytes);
for (let i = 0; i < columnOrder.length; i++) { for (let i = 0; i < columnOrder.length; i++) {
if (colData[i] === null) { if (colData[i] === null) {
nullBitmap[Math.floor(i / 8)] |= (1 << (i % 8)); nullBitmap[Math.floor(i / 8)] |= (1 << (i % 8));
} }
} }
buf.set(nullBitmap, offset); buf.set(nullBitmap, offset);
offset += nullBitmapBytes; offset += nullBitmapBytes;
// Column data // Column data
for (let i = 0; i < columnOrder.length; i++) { for (let i = 0; i < columnOrder.length; i++) {
const encoded = colData[i]; const encoded = colData[i];
if (encoded === null) { if (encoded === null) {
view.setUint8(offset, ColumnEncoding.NULL); view.setUint8(offset, ColumnEncoding.NULL);
offset += 1; offset += 1;
} else { } else {
const tag = getEncodingTag(columnTypes[columnOrder[i]] ?? 'string'); const tag = getEncodingTag(columnTypes[columnOrder[i]] ?? 'string');
view.setUint8(offset, tag); view.setUint8(offset, tag);
offset += 1; offset += 1;
buf.set(encoded, offset); buf.set(encoded, offset);
offset += encoded.byteLength; offset += encoded.byteLength;
} }
} }
return buf; return buf;
} }
/** /**
* *
*/ */
function encodeColumn(value: unknown, fieldType: string): Uint8Array | null { function encodeColumn(value: unknown, fieldType: string): Uint8Array | null {
if (value === null || value === undefined) return null; if (value === null || value === undefined) return null;
switch (fieldType) { switch (fieldType) {
case 'string': { case 'string': {
const str = String(value); const str = String(value);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const bytes = encoder.encode(str); const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength); const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false); new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2); buf.set(bytes, 2);
return buf; return buf;
} }
case 'number': { case 'number': {
const buf = new ArrayBuffer(8); const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, Number(value), false); new DataView(buf).setFloat64(0, Number(value), false);
return new Uint8Array(buf); return new Uint8Array(buf);
} }
case 'boolean': { case 'boolean': {
return new Uint8Array([value ? 1 : 0]); return new Uint8Array([value ? 1 : 0]);
} }
case 'date': { case 'date': {
const ts = value instanceof Date ? value.getTime() : new Date(String(value)).getTime(); const ts = value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
const buf = new ArrayBuffer(8); const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, ts, false); new DataView(buf).setFloat64(0, ts, false);
return new Uint8Array(buf); return new Uint8Array(buf);
} }
case 'json': { case 'json': {
const str = JSON.stringify(value); const str = JSON.stringify(value);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const bytes = encoder.encode(str); const bytes = encoder.encode(str);
const buf = new Uint8Array(2 + bytes.byteLength); const buf = new Uint8Array(2 + bytes.byteLength);
new DataView(buf.buffer).setUint16(0, bytes.byteLength, false); new DataView(buf.buffer).setUint16(0, bytes.byteLength, false);
buf.set(bytes, 2); buf.set(bytes, 2);
return buf; return buf;
} }
default: default:
return null; return null;
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 解码 // 解码
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* *
* @returns null * @returns null
*/ */
export function decodeTuple( export function decodeTuple(
bytes: Uint8Array, bytes: Uint8Array,
columnOrder: string[], columnOrder: string[],
columnTypes: Record<string, string>, columnTypes: Record<string, string>,
): Record<string, unknown> | null { ): Record<string, unknown> | null {
try { try {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let offset = 0; let offset = 0;
const nullBitmapBytes = Math.ceil(columnOrder.length / 8); const nullBitmapBytes = Math.ceil(columnOrder.length / 8);
if (offset + nullBitmapBytes > bytes.byteLength) return null; if (offset + nullBitmapBytes > bytes.byteLength) return null;
const nullBitmap = bytes.slice(offset, offset + nullBitmapBytes); offset += nullBitmapBytes;
offset += nullBitmapBytes;
const row: Record<string, unknown> = {};
const row: Record<string, unknown> = {};
for (let i = 0; i < columnOrder.length; i++) {
for (let i = 0; i < columnOrder.length; i++) { if (offset >= bytes.byteLength) break;
if (offset >= bytes.byteLength) break;
const tag = view.getUint8(offset);
const tag = view.getUint8(offset); offset += 1;
offset += 1;
if (tag === ColumnEncoding.NULL) {
if (tag === ColumnEncoding.NULL) { row[columnOrder[i]] = null;
row[columnOrder[i]] = null; continue;
continue; }
}
const col = columnOrder[i];
const col = columnOrder[i]; const fType = columnTypes[col] ?? 'string';
const fType = columnTypes[col] ?? 'string';
const result = decodeColumnValue(bytes, offset, tag, fType);
const result = decodeColumnValue(bytes, offset, tag, fType); if (result === null) return null;
if (result === null) return null; row[col] = result.value;
row[col] = result.value; offset = result.nextOffset;
offset = result.nextOffset; }
}
return row;
return row; } catch {
} catch { return null;
return null; }
} }
}
function decodeColumnValue(
function decodeColumnValue( bytes: Uint8Array,
bytes: Uint8Array, offset: number,
offset: number, tag: number,
tag: number, _fieldType: string,
fieldType: string, ): { value: unknown; nextOffset: number } | null {
): { value: unknown; nextOffset: number } | null { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
switch (tag) {
switch (tag) { case ColumnEncoding.STRING:
case ColumnEncoding.STRING: case ColumnEncoding.JSON: {
case ColumnEncoding.JSON: { if (offset + 2 > bytes.byteLength) return null;
if (offset + 2 > bytes.byteLength) return null; const len = view.getUint16(offset, false);
const len = view.getUint16(offset, false); offset += 2;
offset += 2; if (offset + len > bytes.byteLength) return null;
if (offset + len > bytes.byteLength) return null; const decoder = new TextDecoder();
const decoder = new TextDecoder(); const str = decoder.decode(bytes.slice(offset, offset + len));
const str = decoder.decode(bytes.slice(offset, offset + len)); return {
return { value: tag === ColumnEncoding.JSON ? JSON.parse(str) : str,
value: tag === ColumnEncoding.JSON ? JSON.parse(str) : str, nextOffset: offset + len,
nextOffset: offset + len, };
}; }
} case ColumnEncoding.NUMBER: {
case ColumnEncoding.NUMBER: { if (offset + 8 > bytes.byteLength) return null;
if (offset + 8 > bytes.byteLength) return null; const val = view.getFloat64(offset, false);
const val = view.getFloat64(offset, false); return { value: val, nextOffset: offset + 8 };
return { value: val, nextOffset: offset + 8 }; }
} case ColumnEncoding.BOOLEAN: {
case ColumnEncoding.BOOLEAN: { if (offset >= bytes.byteLength) return null;
if (offset >= bytes.byteLength) return null; return { value: view.getUint8(offset) !== 0, nextOffset: offset + 1 };
return { value: view.getUint8(offset) !== 0, nextOffset: offset + 1 }; }
} case ColumnEncoding.DATE: {
case ColumnEncoding.DATE: { if (offset + 8 > bytes.byteLength) return null;
if (offset + 8 > bytes.byteLength) return null; const ts = view.getFloat64(offset, false);
const ts = view.getFloat64(offset, false); return { value: new Date(ts).toISOString(), nextOffset: offset + 8 };
return { value: new Date(ts).toISOString(), nextOffset: offset + 8 }; }
} default:
default: return null;
return null; }
} }
}
// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // 辅助
// 辅助 // ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
function getEncodingTag(fieldType: string): ColumnEncoding {
function getEncodingTag(fieldType: string): ColumnEncoding { switch (fieldType) {
switch (fieldType) { case 'string': return ColumnEncoding.STRING;
case 'string': return ColumnEncoding.STRING; case 'number': return ColumnEncoding.NUMBER;
case 'number': return ColumnEncoding.NUMBER; case 'boolean': return ColumnEncoding.BOOLEAN;
case 'boolean': return ColumnEncoding.BOOLEAN; case 'date': return ColumnEncoding.DATE;
case 'date': return ColumnEncoding.DATE; case 'json': return ColumnEncoding.JSON;
case 'json': return ColumnEncoding.JSON; default: return ColumnEncoding.NULL;
default: return ColumnEncoding.NULL; }
} }
}
/** 获取列类型到编码标签的映射 */
/** 获取列类型到编码标签的映射 */ export function getColumnEncodingMap(
export function getColumnEncodingMap( columnOrder: string[],
columnOrder: string[], columnTypes: Record<string, string>,
columnTypes: Record<string, string>, ): Map<string, ColumnEncoding> {
): Map<string, ColumnEncoding> { const map = new Map<string, ColumnEncoding>();
const map = new Map<string, ColumnEncoding>(); for (const col of columnOrder) {
for (const col of columnOrder) { map.set(col, getEncodingTag(columnTypes[col] ?? 'string'));
map.set(col, getEncodingTag(columnTypes[col] ?? 'string')); }
} return map;
return map; }
}
+179 -179
View File
@@ -1,179 +1,179 @@
/** /**
* AriaEngine Storage Backend * AriaEngine Storage Backend
* @module engine/aria/store/backend * @module engine/aria/store/backend
* *
* APIIndexedDB / OPFS / Memory 退 * APIIndexedDB / OPFS / Memory 退
* Buffer Pool PageIO WAL WALStore 使 * Buffer Pool PageIO WAL WALStore 使
*/ */
import { DatabaseError } from '../../../constants'; import { DatabaseError } from '../../../constants';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// StorageBackend 接口 // StorageBackend 接口
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface IStorageBackend { export interface IStorageBackend {
/** 打开存储 */ /** 打开存储 */
open(name: string): Promise<void>; open(name: string): Promise<void>;
/** 关闭存储 */ /** 关闭存储 */
close(): Promise<void>; close(): Promise<void>;
/** 是否已打开 */ /** 是否已打开 */
isOpen(): boolean; isOpen(): boolean;
/** 读取数据块 */ /** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>; read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */ /** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>; write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */ /** 删除数据块 */
delete(key: string): Promise<void>; delete(key: string): Promise<void>;
/** 列出所有 key */ /** 列出所有 key */
listKeys(): Promise<string[]>; listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */ /** 检查 key 是否存在 */
exists(key: string): Promise<boolean>; exists(key: string): Promise<boolean>;
/** 清空所有数据 */ /** 清空所有数据 */
clear(): Promise<void>; clear(): Promise<void>;
} }
// ======================================================================= // =======================================================================
// IndexedDB Backend // IndexedDB Backend
// ======================================================================= // =======================================================================
export class IndexedDBBackend implements IStorageBackend { export class IndexedDBBackend implements IStorageBackend {
private db: IDBDatabase | null = null; private db: IDBDatabase | null = null;
private dbName = ''; private dbName = '';
private storeName = 'data'; private storeName = 'data';
async open(name: string): Promise<void> { async open(name: string): Promise<void> {
this.dbName = `aria-${name}`; this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1); const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
const db = request.result; const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) { if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName); db.createObjectStore(this.storeName);
} }
}; };
request.onsuccess = () => { request.onsuccess = () => {
this.db = request.result; this.db = request.result;
resolve(); resolve();
}; };
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR')); request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
}); });
} }
async close(): Promise<void> { async close(): Promise<void> {
if (this.db) { if (this.db) {
this.db.close(); this.db.close();
this.db = null; this.db = null;
} }
} }
isOpen(): boolean { isOpen(): boolean {
return this.db !== null; return this.db !== null;
} }
async read(key: string): Promise<ArrayBuffer | null> { async read(key: string): Promise<ArrayBuffer | null> {
const db = this.ensureDB(); const db = this.ensureDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly'); const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key); const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null); req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR')); req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
}); });
} }
async write(key: string, data: ArrayBuffer): Promise<void> { async write(key: string, data: ArrayBuffer): Promise<void> {
const db = this.ensureDB(); const db = this.ensureDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite'); const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key); tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve(); tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR')); tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
}); });
} }
async delete(key: string): Promise<void> { async delete(key: string): Promise<void> {
const db = this.ensureDB(); const db = this.ensureDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite'); const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key); tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve(); tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR')); tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
}); });
} }
async listKeys(): Promise<string[]> { async listKeys(): Promise<string[]> {
const db = this.ensureDB(); const db = this.ensureDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly'); const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys(); const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []) as string[]); req.onsuccess = () => resolve((req.result ?? []) as string[]);
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR')); req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
}); });
} }
async exists(key: string): Promise<boolean> { async exists(key: string): Promise<boolean> {
const result = await this.read(key); const result = await this.read(key);
return result !== null; return result !== null;
} }
async clear(): Promise<void> { async clear(): Promise<void> {
const db = this.ensureDB(); const db = this.ensureDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite'); const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear(); tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve(); tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR')); tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
}); });
} }
private ensureDB(): IDBDatabase { private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN'); if (!this.db) throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db; return this.db;
} }
} }
// ======================================================================= // =======================================================================
// Memory Backend(回退 / 测试用) // Memory Backend(回退 / 测试用)
// ======================================================================= // =======================================================================
export class MemoryBackend implements IStorageBackend { export class MemoryBackend implements IStorageBackend {
private store: Map<string, ArrayBuffer> = new Map(); private store: Map<string, ArrayBuffer> = new Map();
private opened = false; private opened = false;
async open(_name: string): Promise<void> { async open(_name: string): Promise<void> {
this.opened = true; this.opened = true;
} }
async close(): Promise<void> { async close(): Promise<void> {
this.store.clear(); this.store.clear();
this.opened = false; this.opened = false;
} }
isOpen(): boolean { isOpen(): boolean {
return this.opened; return this.opened;
} }
async read(key: string): Promise<ArrayBuffer | null> { async read(key: string): Promise<ArrayBuffer | null> {
return this.store.get(key) ?? null; return this.store.get(key) ?? null;
} }
async write(key: string, data: ArrayBuffer): Promise<void> { async write(key: string, data: ArrayBuffer): Promise<void> {
this.store.set(key, data); this.store.set(key, data);
} }
async delete(key: string): Promise<void> { async delete(key: string): Promise<void> {
this.store.delete(key); this.store.delete(key);
} }
async listKeys(): Promise<string[]> { async listKeys(): Promise<string[]> {
return Array.from(this.store.keys()); return Array.from(this.store.keys());
} }
async exists(key: string): Promise<boolean> { async exists(key: string): Promise<boolean> {
return this.store.has(key); return this.store.has(key);
} }
async clear(): Promise<void> { async clear(): Promise<void> {
this.store.clear(); this.store.clear();
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
/** /**
* AriaEngine File Manager + PageIO * AriaEngine File Manager + PageIO
* @module engine/aria/store/file_manager * @module engine/aria/store/file_manager
* *
* / ID * / ID
*/ */
import type { IStorageBackend } from './backend'; import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool'; import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types'; import { PAGE_SIZE, PageType } from '../types';
import { initPageHeader } from '../page/header'; import { initPageHeader } from '../page/header';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// FileManager (implements PageIO) // FileManager (implements PageIO)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class FileManager implements PageIO { export class FileManager implements PageIO {
private backend: IStorageBackend; private backend: IStorageBackend;
private nextPageId = 0; private nextPageId = 0;
private metaLoaded = false; private metaLoaded = false;
private dbName = ''; private dbName = '';
constructor(backend: IStorageBackend) { constructor(backend: IStorageBackend) {
this.backend = backend; this.backend = backend;
} }
/** 初始化:从存储中读取元数据 */ /** 初始化:从存储中读取元数据 */
async init(dbName: string): Promise<void> { async init(dbName: string): Promise<void> {
this.dbName = dbName; this.dbName = dbName;
const meta = await this.backend.read('__aria_meta'); const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) { if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta); const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false); this.nextPageId = view.getUint32(0, false);
} else { } else {
this.nextPageId = 1; this.nextPageId = 1;
await this.saveMeta(); await this.saveMeta();
} }
this.metaLoaded = true; this.metaLoaded = true;
} }
// ---- PageIO ---- // ---- PageIO ----
async readPage(pageId: number): Promise<ArrayBuffer | null> { async readPage(pageId: number): Promise<ArrayBuffer | null> {
const key = `pg_${pageId}`; const key = `pg_${pageId}`;
const data = await this.backend.read(key); const data = await this.backend.read(key);
if (!data) { if (!data) {
// 第一次访问:创建新页面 // 第一次访问:创建新页面
return this.createEmptyPage(pageId, PageType.DATA); return this.createEmptyPage(pageId, PageType.DATA);
} }
// 确保大小正确 // 确保大小正确
if (data.byteLength < PAGE_SIZE) { if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE); const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data)); new Uint8Array(padded).set(new Uint8Array(data));
return padded; return padded;
} }
return data; return data;
} }
async writePage(pageId: number, data: ArrayBuffer): Promise<void> { async writePage(pageId: number, data: ArrayBuffer): Promise<void> {
const key = `pg_${pageId}`; const key = `pg_${pageId}`;
await this.backend.write(key, data); await this.backend.write(key, data);
} }
async allocatePageId(): Promise<number> { async allocatePageId(): Promise<number> {
const id = this.nextPageId++; const id = this.nextPageId++;
await this.saveMeta(); await this.saveMeta();
return id; return id;
} }
async freePageId(_pageId: number): Promise<void> { async freePageId(_pageId: number): Promise<void> {
// 简化实现:不回收 pageId // 简化实现:不回收 pageId
const key = `pg_${_pageId}`; const key = `pg_${_pageId}`;
await this.backend.delete(key); await this.backend.delete(key);
} }
// ---- 表页面分配 ---- // ---- 表页面分配 ----
/** /**
* *
*/ */
async allocateTableRootPage(): Promise<number> { async allocateTableRootPage(): Promise<number> {
const pageId = await this.allocatePageId(); const pageId = await this.allocatePageId();
const data = new ArrayBuffer(PAGE_SIZE); const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, PageType.META); initPageHeader(data, pageId, PageType.META);
await this.writePage(pageId, data); await this.writePage(pageId, data);
return pageId; return pageId;
} }
// ---- 辅助 ---- // ---- 辅助 ----
private async saveMeta(): Promise<void> { private async saveMeta(): Promise<void> {
const buf = new ArrayBuffer(8); const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false); new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf); await this.backend.write('__aria_meta', buf);
} }
private createEmptyPage(pageId: number, type: PageType): ArrayBuffer { private createEmptyPage(pageId: number, type: PageType): ArrayBuffer {
const buf = new ArrayBuffer(PAGE_SIZE); const buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, pageId, type); initPageHeader(buf, pageId, type);
return buf; return buf;
} }
/** 清空所有数据 */ /** 清空所有数据 */
async clearAll(): Promise<void> { async clearAll(): Promise<void> {
await this.backend.clear(); await this.backend.clear();
this.nextPageId = 1; this.nextPageId = 1;
await this.saveMeta(); await this.saveMeta();
} }
} }
+243 -243
View File
@@ -1,243 +1,243 @@
/** /**
* AriaEngine MVCC * AriaEngine MVCC
* @module engine/aria/transaction/mvcc * @module engine/aria/transaction/mvcc
* *
* (Snapshot Isolation) * (Snapshot Isolation)
* *
*/ */
import type { RowVersion, TxnEntry } from '../types'; import type { RowVersion, TxnEntry } from '../types';
import { TransactionState } from '../types'; import { TransactionState } from '../types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MVCCManager // MVCCManager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class MVCCManager { export class MVCCManager {
/** 所有行版本的存储:tableName.key → 版本链 */ /** 所有行版本的存储:tableName.key → 版本链 */
private versionStore: Map<string, RowVersion[]> = new Map(); private versionStore: Map<string, RowVersion[]> = new Map();
/** 活跃事务表:txnId → TxnEntry */ /** 活跃事务表:txnId → TxnEntry */
private activeTxns: Map<number, TxnEntry> = new Map(); private activeTxns: Map<number, TxnEntry> = new Map();
/** 事务 ID 计数器 */ /** 事务 ID 计数器 */
private nextTxnId = 1; private nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */ /** 全局提交序列号(用于可见性判断) */
private globalCommitLsn = 0; private globalCommitLsn = 0;
// ======================================================================= // =======================================================================
// 事务管理 // 事务管理
// ======================================================================= // =======================================================================
/** 开始一个事务,返回事务 ID */ /** 开始一个事务,返回事务 ID */
beginTransaction(): number { beginTransaction(): number {
const txnId = this.nextTxnId++; const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, { this.activeTxns.set(txnId, {
txnId, txnId,
state: TransactionState.ACTIVE, state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn, snapshotLsn: this.globalCommitLsn,
startTime: Date.now(), startTime: Date.now(),
}); });
return txnId; return txnId;
} }
/** 提交事务 */ /** 提交事务 */
commitTransaction(txnId: number): void { commitTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId); const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`); if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED; txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++; this.globalCommitLsn++;
// 标记此事务写入的所有版本为已提交 // 标记此事务写入的所有版本为已提交
for (const [, versions] of this.versionStore) { for (const [, versions] of this.versionStore) {
for (const version of versions) { for (const version of versions) {
if (version.txnId === txnId) { if (version.txnId === txnId) {
version.committed = true; version.committed = true;
} }
} }
} }
// 清理已提交事务的记录 // 清理已提交事务的记录
this.activeTxns.delete(txnId); this.activeTxns.delete(txnId);
} }
/** 回滚事务 */ /** 回滚事务 */
rollbackTransaction(txnId: number): void { rollbackTransaction(txnId: number): void {
const txn = this.activeTxns.get(txnId); const txn = this.activeTxns.get(txnId);
if (!txn) throw new Error(`Transaction ${txnId} not found`); if (!txn) throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED; txn.state = TransactionState.ABORTED;
// 移除此事务写入的所有版本 // 移除此事务写入的所有版本
for (const [tableKey, versions] of this.versionStore) { for (const [tableKey, versions] of this.versionStore) {
const filtered = versions.filter((v) => v.txnId !== txnId); const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) { if (filtered.length === 0) {
this.versionStore.delete(tableKey); this.versionStore.delete(tableKey);
} else { } else {
this.versionStore.set(tableKey, filtered); this.versionStore.set(tableKey, filtered);
} }
} }
this.activeTxns.delete(txnId); this.activeTxns.delete(txnId);
} }
/** 检查事务是否活跃 */ /** 检查事务是否活跃 */
isActive(txnId: number): boolean { isActive(txnId: number): boolean {
const txn = this.activeTxns.get(txnId); const txn = this.activeTxns.get(txnId);
return txn !== undefined && txn.state === TransactionState.ACTIVE; return txn !== undefined && txn.state === TransactionState.ACTIVE;
} }
// ======================================================================= // =======================================================================
// 版本读写 // 版本读写
// ======================================================================= // =======================================================================
/** /**
* *
*/ */
writeVersion( writeVersion(
tableName: string, tableName: string,
key: string, key: string,
data: Record<string, unknown>, data: Record<string, unknown>,
txnId: number, txnId: number,
): void { ): void {
const tableKey = `${tableName}.${key}`; const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? []; const versions = this.versionStore.get(tableKey) ?? [];
const newVersion: RowVersion = { const newVersion: RowVersion = {
txnId, txnId,
data, data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null, prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false, committed: false,
}; };
versions.push(newVersion); versions.push(newVersion);
this.versionStore.set(tableKey, versions); this.versionStore.set(tableKey, versions);
} }
/** /**
* *
*/ */
readVersion( readVersion(
tableName: string, tableName: string,
key: string, key: string,
txnId: number, txnId: number,
): Record<string, unknown> | null { ): Record<string, unknown> | null {
const txn = this.activeTxns.get(txnId); const txn = this.activeTxns.get(txnId);
if (!txn) return null; if (!txn) return null;
const tableKey = `${tableName}.${key}`; const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey); const versions = this.versionStore.get(tableKey);
if (!versions || versions.length === 0) return null; if (!versions || versions.length === 0) return null;
// 从最新版本向前遍历 // 从最新版本向前遍历
for (let i = versions.length - 1; i >= 0; i--) { for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i]; const version = versions[i];
// 1. 如果是当前事务写入的(未提交),可见 // 1. 如果是当前事务写入的(未提交),可见
if (version.txnId === txnId) { if (version.txnId === txnId) {
return version.data; return version.data;
} }
// 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见 // 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见
if (version.committed) { if (version.committed) {
// 简化:所有已提交版本都可见 // 简化:所有已提交版本都可见
return version.data; return version.data;
} }
// 3. 其他事务的未提交版本,不可见,继续找更早的版本 // 3. 其他事务的未提交版本,不可见,继续找更早的版本
} }
return null; return null;
} }
/** /**
* *
*/ */
deleteVersion(tableName: string, key: string, txnId: number): void { deleteVersion(tableName: string, key: string, txnId: number): void {
this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId); this.writeVersion(tableName, key, { __mvcc_tombstone: true } as unknown as Record<string, unknown>, txnId);
} }
/** /**
* *
*/ */
getLatestCommittedVersions( getLatestCommittedVersions(
tableName: string, tableName: string,
): Record<string, Record<string, unknown>> { ): Record<string, Record<string, unknown>> {
const result: Record<string, Record<string, unknown>> = {}; const result: Record<string, Record<string, unknown>> = {};
for (const [tableKey, versions] of this.versionStore) { for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(`${tableName}.`)) continue; if (!tableKey.startsWith(`${tableName}.`)) continue;
const key = tableKey.slice(tableName.length + 1); const key = tableKey.slice(tableName.length + 1);
for (let i = versions.length - 1; i >= 0; i--) { for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i]; const version = versions[i];
if (version.committed && !(version.data as unknown as Record<string, unknown>).__mvcc_tombstone) { if (version.committed && !(version.data as unknown as Record<string, unknown>).__mvcc_tombstone) {
result[key] = version.data; result[key] = version.data;
break; break;
} }
} }
} }
return result; return result;
} }
/** /**
* GC * GC
* key N * key N
*/ */
gc(maxVersionsPerKey: number = 100): void { gc(maxVersionsPerKey: number = 100): void {
for (const [tableKey, versions] of this.versionStore) { for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey) continue; if (versions.length <= maxVersionsPerKey) continue;
// 保留最新的 maxVersionsPerKey 个版本 // 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey); const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned); this.versionStore.set(tableKey, pruned);
} }
} }
/** /**
* key * key
*/ */
getActiveWriteKeys(tableName: string, txnId: number): Set<string> { getActiveWriteKeys(tableName: string, txnId: number): Set<string> {
const keys = new Set<string>(); const keys = new Set<string>();
const prefix = `${tableName}.`; const prefix = `${tableName}.`;
for (const [tableKey, versions] of this.versionStore) { for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(prefix)) continue; if (!tableKey.startsWith(prefix)) continue;
const latestVersion = versions[versions.length - 1]; const latestVersion = versions[versions.length - 1];
if (latestVersion.txnId === txnId && !latestVersion.committed) { if (latestVersion.txnId === txnId && !latestVersion.committed) {
keys.add(tableKey.slice(prefix.length)); keys.add(tableKey.slice(prefix.length));
} }
} }
return keys; return keys;
} }
/** /**
* *
*/ */
clearTable(tableName: string): void { clearTable(tableName: string): void {
const prefix = `${tableName}.`; const prefix = `${tableName}.`;
for (const [tableKey] of this.versionStore) { for (const [tableKey] of this.versionStore) {
if (tableKey.startsWith(prefix)) { if (tableKey.startsWith(prefix)) {
this.versionStore.delete(tableKey); this.versionStore.delete(tableKey);
} }
} }
} }
/** /**
* *
*/ */
getActiveTxnCount(): number { getActiveTxnCount(): number {
return this.activeTxns.size; return this.activeTxns.size;
} }
/** /**
* LSN * LSN
*/ */
getGlobalLSN(): number { getGlobalLSN(): number {
return this.globalCommitLsn; return this.globalCommitLsn;
} }
} }
+302 -302
View File
@@ -1,302 +1,302 @@
/** /**
* AriaEngine Types * AriaEngine Types
* @module engine/aria/types * @module engine/aria/types
* *
* *
*/ */
// ============================================================================= // =============================================================================
// 页面常量 // 页面常量
// ============================================================================= // =============================================================================
/** 页面大小:4KB */ /** 页面大小:4KB */
export const PAGE_SIZE = 4096; export const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */ /** 页面头大小:16 字节 */
export const PAGE_HEADER_SIZE = 16; export const PAGE_HEADER_SIZE = 16;
/** 每个 Slot 目录项大小:4 字节 (offset: u16 + len: u16) */ /** 每个 Slot 目录项大小:4 字节 (offset: u16 + len: u16) */
export const SLOT_ENTRY_SIZE = 4; export const SLOT_ENTRY_SIZE = 4;
/** 页面数据区起始偏移(头部之后) */ /** 页面数据区起始偏移(头部之后) */
export const PAGE_DATA_START = PAGE_HEADER_SIZE; export const PAGE_DATA_START = PAGE_HEADER_SIZE;
/** 无效页面 ID */ /** 无效页面 ID */
export const INVALID_PAGE_ID = 0xFFFFFFFF; export const INVALID_PAGE_ID = 0xFFFFFFFF;
// ============================================================================= // =============================================================================
// 页面类型 // 页面类型
// ============================================================================= // =============================================================================
export enum PageType { export enum PageType {
/** 数据页面:存储行数据 */ /** 数据页面:存储行数据 */
DATA = 1, DATA = 1,
/** 索引页面:存储索引节点 */ /** 索引页面:存储索引节点 */
INDEX = 2, INDEX = 2,
/** 溢出页面:存储大字段 */ /** 溢出页面:存储大字段 */
OVERFLOW = 3, OVERFLOW = 3,
/** 元数据页面:存储表/库元信息 */ /** 元数据页面:存储表/库元信息 */
META = 4, META = 4,
} }
// ============================================================================= // =============================================================================
// 页面头部(16 字节) // 页面头部(16 字节)
// ============================================================================= // =============================================================================
export interface PageHeader { export interface PageHeader {
/** 页面 ID(全局唯一) */ /** 页面 ID(全局唯一) */
pageId: number; pageId: number;
/** 页面类型 */ /** 页面类型 */
type: PageType; type: PageType;
/** 空闲空间起始偏移(slot 区结束位置) */ /** 空闲空间起始偏移(slot 区结束位置) */
freeStart: number; freeStart: number;
/** 数据区结束偏移(从页面底部向上增长) */ /** 数据区结束偏移(从页面底部向上增长) */
freeEnd: number; freeEnd: number;
/** 当前 slot 数量 */ /** 当前 slot 数量 */
slotCount: number; slotCount: number;
/** CRC32 校验和 */ /** CRC32 校验和 */
checksum: number; checksum: number;
} }
// ============================================================================= // =============================================================================
// Slot 目录项(4 字节) // Slot 目录项(4 字节)
// ============================================================================= // =============================================================================
export interface SlotEntry { export interface SlotEntry {
/** 行数据在页面内的偏移 */ /** 行数据在页面内的偏移 */
offset: number; offset: number;
/** 行数据长度(字节) */ /** 行数据长度(字节) */
length: number; length: number;
} }
// ============================================================================= // =============================================================================
// 页面句柄(Buffer Pool 中的页面) // 页面句柄(Buffer Pool 中的页面)
// ============================================================================= // =============================================================================
export interface PageHandle { export interface PageHandle {
/** 页面 ID */ /** 页面 ID */
pageId: number; pageId: number;
/** 页面类型 */ /** 页面类型 */
type: PageType; type: PageType;
/** 页面数据缓冲区(4KB ArrayBuffer */ /** 页面数据缓冲区(4KB ArrayBuffer */
data: ArrayBuffer; data: ArrayBuffer;
/** 是否被修改(脏页) */ /** 是否被修改(脏页) */
dirty: boolean; dirty: boolean;
/** 引用计数(pin count */ /** 引用计数(pin count */
pins: number; pins: number;
/** LRU 链表前驱 */ /** LRU 链表前驱 */
prev: PageHandle | null; prev: PageHandle | null;
/** LRU 链表后继 */ /** LRU 链表后继 */
next: PageHandle | null; next: PageHandle | null;
/** 最后访问时间戳 */ /** 最后访问时间戳 */
lastAccess: number; lastAccess: number;
} }
// ============================================================================= // =============================================================================
// 行编解码 // 行编解码
// ============================================================================= // =============================================================================
/** 行/元组的二进制表示 */ /** 行/元组的二进制表示 */
export interface SerializedTuple { export interface SerializedTuple {
/** 序列化后的字节数组 */ /** 序列化后的字节数组 */
bytes: Uint8Array; bytes: Uint8Array;
/** 该行中 null 列的位图 */ /** 该行中 null 列的位图 */
nullBitmap: Uint8Array; nullBitmap: Uint8Array;
} }
/** 列类型(内部二进制编码用) */ /** 列类型(内部二进制编码用) */
export enum ColumnEncoding { export enum ColumnEncoding {
STRING = 1, STRING = 1,
NUMBER = 2, NUMBER = 2,
BOOLEAN = 3, BOOLEAN = 3,
DATE = 4, DATE = 4,
JSON = 5, JSON = 5,
NULL = 6, NULL = 6,
} }
// ============================================================================= // =============================================================================
// LSM-Tree // LSM-Tree
// ============================================================================= // =============================================================================
/** MemTable 最大大小(默认 4MB */ /** MemTable 最大大小(默认 4MB */
export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024; export const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** SSTable 中每个 Data Block 的默认大小 */ /** SSTable 中每个 Data Block 的默认大小 */
export const DEFAULT_BLOCK_SIZE = 4096; export const DEFAULT_BLOCK_SIZE = 4096;
/** Bloom Filter 每 key 的默认位数 */ /** Bloom Filter 每 key 的默认位数 */
export const DEFAULT_BLOOM_BITS_PER_KEY = 10; export const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */ /** SSTable 最大层级 */
export const MAX_LSM_LEVELS = 7; export const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */ /** 每层之间的大小倍数 */
export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10; export const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
/** SSTable 元数据 */ /** SSTable 元数据 */
export interface SSTableMeta { export interface SSTableMeta {
/** SSTable 文件 ID */ /** SSTable 文件 ID */
id: number; id: number;
/** 所在层级 */ /** 所在层级 */
level: number; level: number;
/** 最小 key */ /** 最小 key */
minKey: string; minKey: string;
/** 最大 key */ /** 最大 key */
maxKey: string; maxKey: string;
/** 数据块数量 */ /** 数据块数量 */
blockCount: number; blockCount: number;
/** 总大小(字节) */ /** 总大小(字节) */
totalSize: number; totalSize: number;
/** Bloom Filter 序列化数据 */ /** Bloom Filter 序列化数据 */
bloomData: Uint8Array | null; bloomData: Uint8Array | null;
} }
/** SSTable 内部的 Data Block */ /** SSTable 内部的 Data Block */
export interface DataBlock { export interface DataBlock {
/** 该块内的 key-value 条目数 */ /** 该块内的 key-value 条目数 */
entryCount: number; entryCount: number;
/** 该块数据区 */ /** 该块数据区 */
data: Uint8Array; data: Uint8Array;
/** 该块起始 key */ /** 该块起始 key */
startKey: string; startKey: string;
/** 该块结束 key */ /** 该块结束 key */
endKey: string; endKey: string;
} }
/** 索引块条目:key → data block offset */ /** 索引块条目:key → data block offset */
export interface IndexEntry { export interface IndexEntry {
/** 到此 block 的最后一个 key */ /** 到此 block 的最后一个 key */
key: string; key: string;
/** data block 在 SSTable 文件中的偏移 */ /** data block 在 SSTable 文件中的偏移 */
blockOffset: number; blockOffset: number;
/** data block 大小 */ /** data block 大小 */
blockSize: number; blockSize: number;
} }
// ============================================================================= // =============================================================================
// WAL (Write-Ahead Log) // WAL (Write-Ahead Log)
// ============================================================================= // =============================================================================
/** WAL 记录类型 */ /** WAL 记录类型 */
export enum WALRecordType { export enum WALRecordType {
INSERT = 1, INSERT = 1,
UPDATE = 2, UPDATE = 2,
DELETE = 3, DELETE = 3,
BEGIN = 4, BEGIN = 4,
COMMIT = 5, COMMIT = 5,
ROLLBACK = 6, ROLLBACK = 6,
CREATE_TABLE = 7, CREATE_TABLE = 7,
DROP_TABLE = 8, DROP_TABLE = 8,
} }
/** 单条 WAL 记录 */ /** 单条 WAL 记录 */
export interface WALRecord { export interface WALRecord {
/** 日志序列号 */ /** 日志序列号 */
lsn: number; lsn: number;
/** 记录类型 */ /** 记录类型 */
type: WALRecordType; type: WALRecordType;
/** 事务 ID */ /** 事务 ID */
txnId: number; txnId: number;
/** 表名 */ /** 表名 */
tableName: string; tableName: string;
/** 主键值 */ /** 主键值 */
key: string; key: string;
/** 操作数据(INSERT/UPDATE 时有效) */ /** 操作数据(INSERT/UPDATE 时有效) */
data?: Record<string, unknown>; data?: Record<string, unknown>;
/** 校验和 */ /** 校验和 */
checksum: number; checksum: number;
} }
// ============================================================================= // =============================================================================
// MVCC // MVCC
// ============================================================================= // =============================================================================
/** 事务隔离级别 */ /** 事务隔离级别 */
export enum IsolationLevel { export enum IsolationLevel {
READ_COMMITTED = 1, READ_COMMITTED = 1,
SNAPSHOT = 2, SNAPSHOT = 2,
} }
/** 事务状态 */ /** 事务状态 */
export enum TransactionState { export enum TransactionState {
ACTIVE = 1, ACTIVE = 1,
COMMITTED = 2, COMMITTED = 2,
ABORTED = 3, ABORTED = 3,
} }
/** 行版本 */ /** 行版本 */
export interface RowVersion { export interface RowVersion {
/** 事务 ID(创建此版本的事务) */ /** 事务 ID(创建此版本的事务) */
txnId: number; txnId: number;
/** 版本数据 */ /** 版本数据 */
data: Record<string, unknown>; data: Record<string, unknown>;
/** 指向上一版本的指针(undo 链) */ /** 指向上一版本的指针(undo 链) */
prevVersion: RowVersion | null; prevVersion: RowVersion | null;
/** 该版本是否已提交 */ /** 该版本是否已提交 */
committed: boolean; committed: boolean;
} }
/** 活跃事务表项 */ /** 活跃事务表项 */
export interface TxnEntry { export interface TxnEntry {
/** 事务 ID */ /** 事务 ID */
txnId: number; txnId: number;
/** 事务状态 */ /** 事务状态 */
state: TransactionState; state: TransactionState;
/** 快照序列号(用于可见性判断) */ /** 快照序列号(用于可见性判断) */
snapshotLsn: number; snapshotLsn: number;
/** 事务开始时间 */ /** 事务开始时间 */
startTime: number; startTime: number;
} }
// ============================================================================= // =============================================================================
// Buffer Pool // Buffer Pool
// ============================================================================= // =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */ /** Buffer Pool 默认容量:256 页 ≈ 1MB */
export const DEFAULT_BUFFER_POOL_PAGES = 256; export const DEFAULT_BUFFER_POOL_PAGES = 256;
// ============================================================================= // =============================================================================
// AriaEngine 配置 // AriaEngine 配置
// ============================================================================= // =============================================================================
export interface AriaEngineConfig { export interface AriaEngineConfig {
/** 页面大小(默认 4096 */ /** 页面大小(默认 4096 */
pageSize?: number; pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */ /** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number; bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */ /** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number; memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */ /** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number; levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */ /** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number; bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */ /** 是否启用 WAL(默认 true */
walEnabled?: boolean; walEnabled?: boolean;
/** WAL 同步模式 */ /** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none'; walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */ /** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number; checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */ /** 是否启用页面压缩(默认 false) */
compression?: boolean; compression?: boolean;
/** 存储后端 */ /** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory'; storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */ /** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number; walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */ /** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number; maxMemoryMB?: number;
} }
export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = { export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES, bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE, memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER, levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY, bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true, walEnabled: true,
walSyncMode: 'full', walSyncMode: 'full',
checkpointInterval: 1000, checkpointInterval: 1000,
compression: false, compression: false,
storageBackend: 'indexeddb', storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64, maxMemoryMB: 64,
}; };
+77 -77
View File
@@ -1,77 +1,77 @@
/** /**
* AriaEngine Checkpoint * AriaEngine Checkpoint
* @module engine/aria/wal/checkpoint * @module engine/aria/wal/checkpoint
*/ */
import type { LSM } from '../index/lsm'; import type { LSM } from '../index/lsm';
import type { WAL } from './log'; import type { WAL } from './log';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 简化的 flush 接口 // 简化的 flush 接口
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface Flushable { export interface Flushable {
flushAll(): Promise<void>; flushAll(): Promise<void>;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// CheckpointManager // CheckpointManager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export class CheckpointManager { export class CheckpointManager {
private lsm: LSM; private lsm: LSM;
private wal: WAL; private wal: WAL;
private flushable: Flushable | null; private flushable: Flushable | null;
private interval: number; private interval: number;
private opCount = 0; private opCount = 0;
private walSizeThreshold: number; private walSizeThreshold: number;
constructor( constructor(
lsm: LSM, lsm: LSM,
wal: WAL, wal: WAL,
flushable: Flushable | null = null, flushable: Flushable | null = null,
interval: number = 1000, interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024, walSizeThreshold: number = 16 * 1024 * 1024,
) { ) {
this.lsm = lsm; this.lsm = lsm;
this.wal = wal; this.wal = wal;
this.flushable = flushable; this.flushable = flushable;
this.interval = interval; this.interval = interval;
this.walSizeThreshold = walSizeThreshold; this.walSizeThreshold = walSizeThreshold;
} }
async tick(): Promise<void> { async tick(): Promise<void> {
this.opCount++; this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值 // 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) { if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint(); await this.checkpoint();
} }
} }
/** 估算 WAL 大小 */ /** 估算 WAL 大小 */
private getWALEstimatedSize(): number { private getWALEstimatedSize(): number {
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0; const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
return count * 200; return count * 200;
} }
async checkpoint(): Promise<void> { async checkpoint(): Promise<void> {
await this.lsm.flush(); await this.lsm.flush();
if (this.flushable) { if (this.flushable) {
await this.flushable.flushAll(); await this.flushable.flushAll();
} }
await this.wal.checkpoint(); await this.wal.checkpoint();
this.opCount = 0; this.opCount = 0;
} }
async forceCheckpoint(): Promise<void> { async forceCheckpoint(): Promise<void> {
await this.checkpoint(); await this.checkpoint();
} }
setInterval(ops: number): void { setInterval(ops: number): void {
this.interval = ops; this.interval = ops;
} }
getOpCount(): number { getOpCount(): number {
return this.opCount; return this.opCount;
} }
} }
+313 -283
View File
@@ -1,283 +1,313 @@
/** /**
* AriaEngine WAL Write-Ahead Log * AriaEngine WAL Write-Ahead Log
* @module engine/aria/wal/log * @module engine/aria/wal/log
* *
* *
* *
* WAL : * WAL :
* *
* Record 1 Record 2 ... * Record 1 Record 2 ...
* 4B LSN * 4B LSN
* 1B type * 1B type
* 4B txnId * 4B txnId
* 2B tblLen * 2B tblLen
* N table * N table
* 2B keyLen * 2B keyLen
* N key * N key
* 4B jsonLen * 4B jsonLen
* N json * N json
* 4B CRC * 4B CRC
* *
*/ */
import { WALRecordType, type WALRecord } from '../types'; import { WALRecordType, type WALRecord } from '../types';
import type { BufferPool } from '../buffer/pool';
// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // WAL 存储接口
// WAL 存储接口 // ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
export interface WALStore {
export interface WALStore { /** 追加 WAL 记录 */
/** 追加 WAL 记录 */ append(data: Uint8Array): Promise<void>;
append(data: Uint8Array): Promise<void>; /** 读取所有 WAL 记录 */
/** 读取所有 WAL 记录 */ readAll(): Promise<Uint8Array>;
readAll(): Promise<Uint8Array>; /** 截断 WALcheckpoint 后清理) */
/** 截断 WALcheckpoint 后清理) */ truncate(): Promise<void>;
truncate(): Promise<void>; /** 检查 WAL 是否存在 */
/** 检查 WAL 是否存在 */ exists(): Promise<boolean>;
exists(): Promise<boolean>; }
}
// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // WAL
// WAL // ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
export class WAL {
export class WAL { private lsn = 0;
private lsn = 0; private store: WALStore;
private store: WALStore; private enabled: boolean;
private enabled: boolean; private buffer: Uint8Array[] = [];
private buffer: Uint8Array[] = []; private syncMode: 'full' | 'batch' | 'none';
private syncMode: 'full' | 'batch' | 'none';
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') {
constructor(store: WALStore, enabled: boolean = true, syncMode: 'full' | 'batch' | 'none' = 'batch') { this.store = store;
this.store = store; this.enabled = enabled;
this.enabled = enabled; this.syncMode = syncMode;
this.syncMode = syncMode; }
}
// =======================================================================
// ======================================================================= // 写入
// 写入 // =======================================================================
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */ async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> { if (!this.enabled) return;
if (!this.enabled) return;
this.lsn++;
this.lsn++; const fullRecord: WALRecord = {
const fullRecord: WALRecord = { ...record,
...record, lsn: this.lsn,
lsn: this.lsn, checksum: 0, // 稍后计算
checksum: 0, // 稍后计算 };
};
const bytes = this.encodeRecord(fullRecord);
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
if (this.syncMode === 'full') { try {
try { await this.store.append(bytes);
await this.store.append(bytes); } catch {
} catch { // eslint-disable-next-line no-console
// eslint-disable-next-line no-console console.warn('[AriaEngine WAL] Failed to append record');
console.warn('[AriaEngine WAL] Failed to append record'); }
} } else if (this.syncMode === 'batch') {
} else if (this.syncMode === 'batch') { this.buffer.push(bytes);
this.buffer.push(bytes); }
} // 'none' mode: 不写 WAL
// 'none' mode: 不写 WAL }
}
/** 批量追加多条 WAL 记录(组提交:合并为一次底层写入,v0.3.1) */
/** 批量刷新缓冲的 WAL 记录 */ async appendBatch(records: Omit<WALRecord, 'lsn' | 'checksum'>[]): Promise<void> {
async flush(): Promise<void> { if (!this.enabled || records.length === 0) return;
if (!this.enabled || this.buffer.length === 0) return;
const chunks: Uint8Array[] = [];
const totalLen = this.buffer.reduce((sum, b) => sum + b.byteLength, 0); for (const record of records) {
const combined = new Uint8Array(totalLen); this.lsn++;
let offset = 0; chunks.push(this.encodeRecord({ ...record, lsn: this.lsn, checksum: 0 }));
for (const buf of this.buffer) { }
combined.set(buf, offset); const combined = this.mergeChunks(chunks);
offset += buf.byteLength;
} if (this.syncMode === 'full') {
try {
await this.store.append(combined); await this.store.append(combined);
this.buffer = []; } 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);
}
/** 从 WAL 恢复未提交的事务数据 */ // 'none' mode: 不写 WAL
async recover( }
applyRecord: (record: WALRecord) => void,
): Promise<number> { /** 批量刷新缓冲的 WAL 记录 */
if (!this.enabled) return 0; async flush(): Promise<void> {
if (!this.enabled || this.buffer.length === 0) return;
const exists = await this.store.exists();
if (!exists) return 0; const combined = this.mergeChunks(this.buffer);
await this.store.append(combined);
const data = await this.store.readAll(); this.buffer = [];
if (data.byteLength === 0) return 0; }
const records = this.decodeAllRecords(data); /** 合并多个字节块为一个连续缓冲区 */
for (const record of records) { private mergeChunks(chunks: Uint8Array[]): Uint8Array {
applyRecord(record); if (chunks.length === 1) return chunks[0];
} const totalLen = chunks.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0; let offset = 0;
return records.length; for (const buf of chunks) {
} combined.set(buf, offset);
offset += buf.byteLength;
// ======================================================================= }
// Checkpoint return combined;
// ======================================================================= }
/** Checkpoint 后清空 WAL */ // =======================================================================
async checkpoint(): Promise<void> { // 恢复
if (!this.enabled) return; // =======================================================================
await this.flush();
await this.store.truncate(); /** 从 WAL 恢复未提交的事务数据 */
this.lsn = 0; async recover(
} applyRecord: (record: WALRecord) => void,
): Promise<number> {
// ======================================================================= if (!this.enabled) return 0;
// 统计
// ======================================================================= const exists = await this.store.exists();
if (!exists) return 0;
isEnabled(): boolean {
return this.enabled; const data = await this.store.readAll();
} if (data.byteLength === 0) return 0;
getLSN(): number { const records = this.decodeAllRecords(data);
return this.lsn; for (const record of records) {
} applyRecord(record);
}
getBufferedCount(): number {
return this.buffer.length; this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
} return records.length;
}
// -----------------------------------------------------------------------
// 编解码 // =======================================================================
// ----------------------------------------------------------------------- // Checkpoint
// =======================================================================
private encodeRecord(record: WALRecord): Uint8Array {
const encoder = new TextEncoder(); /** Checkpoint 后清空 WAL */
const tableBytes = encoder.encode(record.tableName); async checkpoint(): Promise<void> {
const keyBytes = encoder.encode(record.key); if (!this.enabled) return;
const jsonStr = record.data ? JSON.stringify(record.data) : ''; await this.flush();
const jsonBytes = encoder.encode(jsonStr); await this.store.truncate();
this.lsn = 0;
const size = }
4 + // LSN
1 + // type // =======================================================================
4 + // txnId // 统计
2 + tableBytes.length + // table // =======================================================================
2 + keyBytes.length + // key
4 + jsonBytes.length + // json isEnabled(): boolean {
4; // CRC return this.enabled;
}
const buf = new ArrayBuffer(size);
const view = new DataView(buf); getLSN(): number {
let offset = 0; return this.lsn;
}
view.setUint32(offset, record.lsn, false);
offset += 4; getBufferedCount(): number {
view.setUint8(offset, record.type); return this.buffer.length;
offset += 1; }
view.setUint32(offset, record.txnId, false);
offset += 4; // -----------------------------------------------------------------------
// 编解码
view.setUint16(offset, tableBytes.length, false); // -----------------------------------------------------------------------
offset += 2;
new Uint8Array(buf).set(tableBytes, offset); private encodeRecord(record: WALRecord): Uint8Array {
offset += tableBytes.length; const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
view.setUint16(offset, keyBytes.length, false); const keyBytes = encoder.encode(record.key);
offset += 2; const jsonStr = record.data ? JSON.stringify(record.data) : '';
new Uint8Array(buf).set(keyBytes, offset); const jsonBytes = encoder.encode(jsonStr);
offset += keyBytes.length;
const size =
view.setUint32(offset, jsonBytes.length, false); 4 + // LSN
offset += 4; 1 + // type
new Uint8Array(buf).set(jsonBytes, offset); 4 + // txnId
offset += jsonBytes.length; 2 + tableBytes.length + // table
2 + keyBytes.length + // key
// 简单 CRC 4 + jsonBytes.length + // json
let crc = 0; 4; // CRC
const u8 = new Uint8Array(buf, 0, offset);
for (let i = 0; i < u8.length; i++) { const buf = new ArrayBuffer(size);
crc = ((crc << 5) - crc + u8[i]) | 0; const view = new DataView(buf);
} let offset = 0;
view.setUint32(offset, crc >>> 0, false);
view.setUint32(offset, record.lsn, false);
return new Uint8Array(buf); offset += 4;
} view.setUint8(offset, record.type);
offset += 1;
private decodeAllRecords(data: Uint8Array): WALRecord[] { view.setUint32(offset, record.txnId, false);
const records: WALRecord[] = []; offset += 4;
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0; view.setUint16(offset, tableBytes.length, false);
offset += 2;
while (offset + 15 <= data.byteLength) { new Uint8Array(buf).set(tableBytes, offset);
try { offset += tableBytes.length;
const recordStart = offset;
const lsn = view.getUint32(offset, false); view.setUint16(offset, keyBytes.length, false);
offset += 4; offset += 2;
const type = view.getUint8(offset) as WALRecordType; new Uint8Array(buf).set(keyBytes, offset);
offset += 1; offset += keyBytes.length;
const txnId = view.getUint32(offset, false);
offset += 4; view.setUint32(offset, jsonBytes.length, false);
offset += 4;
const tableLen = view.getUint16(offset, false); new Uint8Array(buf).set(jsonBytes, offset);
offset += 2; offset += jsonBytes.length;
if (offset + tableLen > data.byteLength) break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen)); // 简单 CRC
offset += tableLen; let crc = 0;
const u8 = new Uint8Array(buf, 0, offset);
const keyLen = view.getUint16(offset, false); for (let i = 0; i < u8.length; i++) {
offset += 2; crc = ((crc << 5) - crc + u8[i]) | 0;
if (offset + keyLen > data.byteLength) break; }
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen)); view.setUint32(offset, crc >>> 0, false);
offset += keyLen;
return new Uint8Array(buf);
const jsonLen = view.getUint32(offset, false); }
offset += 4;
if (offset + jsonLen > data.byteLength) break; private decodeAllRecords(data: Uint8Array): WALRecord[] {
let recordData: Record<string, unknown> | undefined; const records: WALRecord[] = [];
if (jsonLen > 0) { const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen)); let offset = 0;
try {
recordData = JSON.parse(json); while (offset + 15 <= data.byteLength) {
} catch { /* ok */ } try {
} const recordStart = offset;
offset += jsonLen; const lsn = view.getUint32(offset, false);
offset += 4;
// 验证 CRC(跨记录数据计算,不含 CRC 自身) const type = view.getUint8(offset) as WALRecordType;
const storedCrc = view.getUint32(offset, false); offset += 1;
offset += 4; const txnId = view.getUint32(offset, false);
const recordBytes = data.slice(recordStart, offset - 4); offset += 4;
let computedCrc = 0;
for (let i = 0; i < recordBytes.length; i++) { const tableLen = view.getUint16(offset, false);
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0; offset += 2;
} if (offset + tableLen > data.byteLength) break;
if ((computedCrc >>> 0) !== storedCrc) { const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
// CRC 不匹配,跳过此损坏记录 offset += tableLen;
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue; const keyLen = view.getUint16(offset, false);
} offset += 2;
if (offset + keyLen > data.byteLength) break;
records.push({ const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
lsn, offset += keyLen;
type,
txnId, const jsonLen = view.getUint32(offset, false);
tableName, offset += 4;
key, if (offset + jsonLen > data.byteLength) break;
data: recordData, let recordData: Record<string, unknown> | undefined;
checksum: storedCrc, if (jsonLen > 0) {
}); const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
} catch { try {
break; recordData = JSON.parse(json);
} } catch { /* ok */ }
} }
offset += jsonLen;
return records;
} // 验证 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 * metona-sqlark Engine
* @module engine * @module engine
*/ */
export type { IStorageEngine } from './interface'; export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory'; export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb'; export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs'; export { OPFSEngine } from './opfs';
export { AriaEngine } from './aria/index'; export { AriaEngine } from './aria/index';
export type { AriaEngineConfig } from './aria/types'; 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); await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version); const request = indexedDB.open(dbName, version);
request.onsuccess = () => { request.onsuccess = async () => {
this.db = request.result; this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接 // 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => { 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.`); 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.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')); 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> { async close(): Promise<void> {
if (this.db) { if (this.db) {
this.db.onversionchange = null; // 清理监听器 this.db.onversionchange = null; // 清理监听器
@@ -113,6 +189,48 @@ export class IndexedDBEngine implements IStorageEngine {
await this.idbClear(tableName); 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> { async beginTransaction(): Promise<void> {
@@ -154,8 +272,19 @@ export class IndexedDBEngine implements IStorageEngine {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false }); 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)); 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; const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(tableName)) db.deleteObjectStore(tableName); 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)); 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; 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>; 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(); 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> { async beginTransaction(): Promise<void> {
@@ -297,10 +332,9 @@ export class MemoryEngine implements IStorageEngine {
for (const [colName, colDef] of Object.entries(refSchema.columns)) { for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onDelete) continue; if (!colDef.references || !colDef.onDelete) continue;
const [refTable, refCol] = colDef.references.split('.'); const [refTable] = colDef.references.split('.');
if (refTable !== tableName) continue; if (refTable !== tableName) continue;
const refPkCol = refCol;
const refTableData = this.tables.get(refTableName); const refTableData = this.tables.get(refTableName);
if (!refTableData) continue; if (!refTableData) continue;
+10
View File
@@ -132,6 +132,16 @@ export class OPFSEngine implements IStorageEngine {
await this.writeTableData(tableName, []); 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> { async beginTransaction(): Promise<void> {
+37 -6
View File
@@ -25,6 +25,8 @@ export class HybridEngine implements IStorageEngine {
private memoryEngine: MemoryEngine; private memoryEngine: MemoryEngine;
private diskEngine: IStorageEngine; private diskEngine: IStorageEngine;
private diskEngineType: DiskEngine; private diskEngineType: DiskEngine;
private dbName = '';
private version = 1;
constructor(diskEngine: DiskEngine = 'indexeddb') { constructor(diskEngine: DiskEngine = 'indexeddb') {
this.memoryEngine = new MemoryEngine(); this.memoryEngine = new MemoryEngine();
@@ -35,6 +37,8 @@ export class HybridEngine implements IStorageEngine {
// ---- 生命周期 ---- // ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> { async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName;
this.version = version;
// 先打开磁盘引擎 // 先打开磁盘引擎
await this.diskEngine.open(dbName, version); await this.diskEngine.open(dbName, version);
@@ -42,6 +46,17 @@ export class HybridEngine implements IStorageEngine {
await this.memoryEngine.open(dbName, version); 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(); const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) { for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName); 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 }); const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) { if (rows.length > 0) {
try { try {
await this.memoryEngine.insert(tableName, rows); await this.memoryEngine.insert(tableName, rows);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e); 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); 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> { async beginTransaction(): Promise<void> {
+91 -91
View File
@@ -1,91 +1,91 @@
/** /**
* metona-sqlark * metona-sqlark
* @module metona-sqlark * @module metona-sqlark
* @version 0.2.5 * @version 0.2.5
* *
* *
* Query Builder API SQL * Query Builder API SQL
*/ */
import { MetonaSqlark } from './core'; import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants'; import type { DatabaseConfig } from './constants';
import { VERSION } from './constants'; import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法) // 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager'; import './connection-manager';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 工厂函数 // 工厂函数
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* *
* *
* @example * @example
* ```ts * ```ts
* const db = await MetonaSqlark.create({ * const db = await MetonaSqlark.create({
* name: 'my-app', * name: 'my-app',
* mode: 'hybrid', * mode: 'hybrid',
* }); * });
* *
* await db.defineTable('users', { * await db.defineTable('users', {
* id: { type: 'string', primaryKey: true }, * id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true }, * name: { type: 'string', required: true },
* }); * });
* *
* await db.table('users').insert({ id: '1', name: 'Alice' }); * await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users'); * const results = await db.query('SELECT * FROM users');
* ``` * ```
*/ */
async function create(config: DatabaseConfig): Promise<MetonaSqlark> { async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config); const db = new MetonaSqlark(config);
await db.init(); await db.init();
return db; return db;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 全局 API // 全局 API
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const api = { const api = {
VERSION, VERSION,
version: VERSION, version: VERSION,
create, create,
MetonaSqlark, MetonaSqlark,
MeSqlark: MetonaSqlark, MeSqlark: MetonaSqlark,
}; };
// 浏览器全局挂载 // 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } } declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.MetonaSqlark = api; window.MetonaSqlark = api;
window.MeSqlark = api; window.MeSqlark = api;
} }
export default api; export default api;
export { export {
api, api,
VERSION, VERSION,
create, create,
MetonaSqlark, MetonaSqlark,
}; };
// 别名 // 别名
export const MeSqlark = MetonaSqlark; export const MeSqlark = MetonaSqlark;
// 类型导出 // 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants'; export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface'; export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast'; export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory'; export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb'; export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs'; export { OPFSEngine } from './engine/opfs';
export { AriaEngine } from './engine/aria/index'; export { AriaEngine } from './engine/aria/index';
export { HybridEngine } from './hybrid/index'; export { HybridEngine } from './hybrid/index';
export { Table } from './table/table'; export { Table } from './table/table';
export { parse } from './sql/parser'; export { parse, parseAll } from './sql/parser';
export { tokenize } from './sql/lexer'; export { tokenize } from './sql/lexer';
// AriaEngine 类型 & 后端 // AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types'; export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend'; 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'; import { useState, useEffect, useCallback, useRef } from 'react';
/** useQuery: 执行 SQL 查询 */ /** useQuery: 执行 SQL 查询 */
+1 -1
View File
@@ -12,7 +12,7 @@
* const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users'); * 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'; import { ref, watch, onMounted, type Ref } from 'vue';
/** useSqlarkQuery: 执行 SQL 查询 */ /** useSqlarkQuery: 执行 SQL 查询 */
+275 -209
View File
@@ -1,209 +1,275 @@
/** /**
* metona-sqlark Query AST * metona-sqlark Query AST
* @module query/ast * @module query/ast
* *
* QueryBuilder SQL Parser AST * QueryBuilder SQL Parser AST
* Executor AST * Executor AST
*/ */
import type { WhereCondition, OrderBy } from '../constants'; import type { WhereCondition, OrderBy } from '../constants';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// AST 语句类型枚举 // AST 语句类型枚举
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export type StatementType = export type StatementType =
| 'SELECT' | 'SELECT'
| 'EXPLAIN' | 'SELECT_UNION'
| 'INSERT' | 'EXPLAIN'
| 'UPDATE' | 'INSERT'
| 'DELETE' | 'UPDATE'
| 'CREATE_TABLE' | 'DELETE'
| 'DROP_TABLE' | 'CREATE_TABLE'
| 'ALTER_TABLE' | 'DROP_TABLE'
| 'TRUNCATE_TABLE'; | 'ALTER_TABLE'
| 'TRUNCATE_TABLE'
// --------------------------------------------------------------------------- | 'CREATE_INDEX'
// 列引用 | 'DROP_INDEX'
// --------------------------------------------------------------------------- | 'BEGIN'
| 'COMMIT'
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */ | 'ROLLBACK';
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // 列引用
// JOIN // ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
/** JOIN 类型 */ export type ColumnRef = string;
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
// ---------------------------------------------------------------------------
/** JOIN 子句 */ // JOIN
export interface JoinClause { // ---------------------------------------------------------------------------
type: JoinType;
table: string; /** JOIN 类型 */
alias?: string; export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
on: WhereCondition;
} /** 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(*) export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
alias?: string;
} /** 聚合表达式 */
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 SubqueryExpression {
// --------------------------------------------------------------------------- type: 'SUBQUERY';
statement: SelectStatement | SelectUnionStatement;
export interface ASTColumnDef { }
name: string;
type: string; // ---------------------------------------------------------------------------
primaryKey?: boolean; // DDL: CREATE TABLE
unique?: boolean; // ---------------------------------------------------------------------------
required?: boolean;
default?: unknown; export interface ASTColumnDef {
index?: boolean; name: string;
maxLength?: number; type: string;
min?: number; primaryKey?: boolean;
max?: number; unique?: boolean;
/** 外键引用 */ required?: boolean;
references?: string; default?: unknown;
/** 级联删除 */ index?: boolean;
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; maxLength?: number;
/** 级联更新 */ min?: number;
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; max?: number;
} /** 外键引用 */
references?: string;
export interface CreateTableStatement { /** 级联删除 */
type: 'CREATE_TABLE'; onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
name: string; /** 级联更新 */
columns: ASTColumnDef[]; onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** IF NOT EXISTS — 表已存在时不报错 */ }
ifNotExists?: boolean;
} export interface CreateTableStatement {
type: 'CREATE_TABLE';
// --------------------------------------------------------------------------- name: string;
// DDL: DROP TABLE columns: ASTColumnDef[];
// --------------------------------------------------------------------------- /** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
export interface DropTableStatement { }
type: 'DROP_TABLE';
name: string; // ---------------------------------------------------------------------------
/** IF EXISTS — 表不存在时不报错 */ // DDL: DROP TABLE
ifExists?: boolean; // ---------------------------------------------------------------------------
}
export interface DropTableStatement {
/** EXPLAIN 查询计划 */ type: 'DROP_TABLE';
export interface ExplainStatement { name: string;
type: 'EXPLAIN'; /** IF EXISTS — 表不存在时不报错 */
query: Statement; ifExists?: boolean;
} }
// --------------------------------------------------------------------------- /** EXPLAIN 查询计划 */
// DML: INSERT export interface ExplainStatement {
// --------------------------------------------------------------------------- type: 'EXPLAIN';
query: Statement;
export interface InsertStatement { }
type: 'INSERT';
into: string; // ---------------------------------------------------------------------------
columns?: string[]; // DML: INSERT
values: unknown[][]; // ---------------------------------------------------------------------------
}
export interface InsertStatement {
// --------------------------------------------------------------------------- type: 'INSERT';
// DML: UPDATE into: string;
// --------------------------------------------------------------------------- columns?: string[];
/** VALUES 字面量 */
export interface UpdateStatement { values?: unknown[][];
type: 'UPDATE'; /** INSERT INTO ... SELECT ...v0.3.0 */
table: string; select?: SelectStatement | SelectUnionStatement;
sets: Record<string, unknown>; }
where: WhereCondition;
} // ---------------------------------------------------------------------------
// DML: UPDATE
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// DML: DELETE
// --------------------------------------------------------------------------- export interface UpdateStatement {
type: 'UPDATE';
export interface DeleteStatement { table: string;
type: 'DELETE'; sets: Record<string, unknown>;
from: string; where: WhereCondition;
where: WhereCondition; }
}
// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // DML: DELETE
// DQL: SELECT // ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
export interface DeleteStatement {
export interface SelectStatement { type: 'DELETE';
type: 'SELECT'; from: string;
columns: ColumnRef[]; where: WhereCondition;
distinct?: boolean; }
from: string;
/** 主表别名 */ // ---------------------------------------------------------------------------
alias?: string; // DQL: SELECT
/** JOIN 子句列表 */ // ---------------------------------------------------------------------------
joins?: JoinClause[];
where: WhereCondition; export interface SelectStatement {
/** GROUP BY */ type: 'SELECT';
groupBy?: string[]; columns: ColumnRef[];
/** HAVING */ distinct?: boolean;
having?: WhereCondition; from: string;
orderBy?: OrderBy[]; /** 主表别名 */
limit?: number; alias?: string;
offset?: number; /** JOIN 子句列表 */
} joins?: JoinClause[];
where: WhereCondition;
// --------------------------------------------------------------------------- /** GROUP BY */
// DDL: ALTER TABLE groupBy?: string[];
// --------------------------------------------------------------------------- /** HAVING */
having?: WhereCondition;
export interface AlterTableStatement { orderBy?: OrderBy[];
type: 'ALTER_TABLE'; limit?: number;
name: string; offset?: number;
action: 'ADD' | 'DROP'; }
column: ASTColumnDef;
} // ---------------------------------------------------------------------------
// DQL: UNION
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// --------------------------------------------------------------------------- export interface SelectUnionStatement {
type: 'SELECT_UNION';
export interface TruncateTableStatement { /** 左操作数(可以是 SELECT 或嵌套 UNION */
type: 'TRUNCATE_TABLE'; left: SelectStatement | SelectUnionStatement;
name: string; /** 右操作数 */
} right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
// --------------------------------------------------------------------------- all?: boolean;
// AST 联合类型 }
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
export type Statement = // DDL: ALTER TABLE
| SelectStatement // ---------------------------------------------------------------------------
| ExplainStatement
| InsertStatement export interface AlterTableStatement {
| UpdateStatement type: 'ALTER_TABLE';
| DeleteStatement name: string;
| CreateTableStatement action: 'ADD' | 'DROP';
| DropTableStatement column: ASTColumnDef;
| AlterTableStatement }
| TruncateTableStatement;
// ---------------------------------------------------------------------------
// 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 { export class UpdateQueryBuilder {
private _where: WhereCondition = {}; private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor( constructor(
private engine: IStorageEngine, private engine: IStorageEngine,
private tableName: string, private tableName: string,
private _updates: Record<string, unknown>, private _updates: Record<string, unknown>,
) {} onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this { where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition }; this._where = { ...this._where, ...condition };
@@ -147,7 +151,9 @@ export class UpdateQueryBuilder {
} }
async execute(): Promise<number> { 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 { toAST(): UpdateStatement {
@@ -161,11 +167,15 @@ export class UpdateQueryBuilder {
export class DeleteQueryBuilder { export class DeleteQueryBuilder {
private _where: WhereCondition = {}; private _where: WhereCondition = {};
private onWrite?: (table: string) => void;
constructor( constructor(
private engine: IStorageEngine, private engine: IStorageEngine,
private tableName: string, private tableName: string,
) {} onWrite?: (table: string) => void,
) {
this.onWrite = onWrite;
}
where(condition: WhereCondition): this { where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition }; this._where = { ...this._where, ...condition };
@@ -173,7 +183,9 @@ export class DeleteQueryBuilder {
} }
async execute(): Promise<number> { 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 { 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 } = {}, options: { $col?: boolean } = {},
): boolean { ): boolean {
for (const [field, condition] of Object.entries(where)) { 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 // 顶层 $and
if (field === '$and') { if (field === '$and') {
const subs = condition as WhereCondition[]; const subs = condition as WhereCondition[];
@@ -54,6 +64,11 @@ export function matchWhere(
if (!subs.some((sub) => matchWhere(row, sub, options))) return false; if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
continue; 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; if (!matchField(row[field], condition, row, options)) return false;
} }
return true; 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', MAX = 'MAX',
DISTINCT = 'DISTINCT', 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', IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING', STRING = 'STRING',
@@ -165,4 +180,19 @@ export const KEYWORDS: Record<string, TokenType> = {
'MIN': TokenType.MIN, 'MIN': TokenType.MIN,
'MAX': TokenType.MAX, 'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT, '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 engine: IStorageEngine;
private schema: TableSchema | null = null; private schema: TableSchema | null = null;
private executor: QueryExecutor | undefined; 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.engine = engine;
this.name = tableName; this.name = tableName;
this.executor = executor; this.executor = executor;
this.onWrite = onWrite;
} }
// ---- Schema ---- // ---- Schema ----
@@ -40,11 +43,14 @@ export class Table<T = Record<string, unknown>> {
async insert(row: T & Record<string, unknown>): Promise<string> { async insert(row: T & Record<string, unknown>): Promise<string> {
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]); const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
this.onWrite?.(this.name);
return pks[0]; return pks[0];
} }
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> { 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 { 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 { 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> { async clear(): Promise<void> {
return this.engine.clear(this.name); await this.engine.clear(this.name);
this.onWrite?.(this.name);
} }
async drop(): Promise<void> { 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 * AriaEngine Buffer Pool + Eviction
*/ */
import { BufferPool, type PageIO } from '../../src/engine/aria/buffer/pool'; import { BufferPool, type PageIO } from '../../src/engine/aria/buffer/pool';
import { LRUList, EvictionManager } from '../../src/engine/aria/buffer/eviction'; import { LRUList, EvictionManager } from '../../src/engine/aria/buffer/eviction';
import { PageType, PAGE_SIZE } from '../../src/engine/aria/types'; import { PageType, PAGE_SIZE } from '../../src/engine/aria/types';
import { initPageHeader } from '../../src/engine/aria/page/header'; import { initPageHeader } from '../../src/engine/aria/page/header';
// =================================================================== // ===================================================================
// LRUList // LRUList
// =================================================================== // ===================================================================
describe('AriaEngine — LRUList', () => { describe('AriaEngine — LRUList', () => {
function makePage(id: number) { function makePage(id: number) {
const data = new ArrayBuffer(PAGE_SIZE); const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA); initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty: false, pins: 0, prev: null, next: null, lastAccess: Date.now() }; return { pageId: id, type: PageType.DATA, data, dirty: false, pins: 0, prev: null, next: null, lastAccess: Date.now() };
} }
it('moveToHead — 单元素', () => { it('moveToHead — 单元素', () => {
const list = new LRUList(); const list = new LRUList();
const p = makePage(1); const p = makePage(1);
list.moveToHead(p); list.moveToHead(p);
expect(list.size).toBe(1); expect(list.size).toBe(1);
}); });
it('moveToHead — 多元素保持 MRU 顺序', () => { it('moveToHead — 多元素保持 MRU 顺序', () => {
const list = new LRUList(); const list = new LRUList();
const a = makePage(1); const a = makePage(1);
const b = makePage(2); const b = makePage(2);
const c = makePage(3); const c = makePage(3);
list.moveToHead(a); list.moveToHead(a);
list.moveToHead(b); list.moveToHead(b);
list.moveToHead(c); list.moveToHead(c);
expect(list.size).toBe(3); expect(list.size).toBe(3);
// c 是最新的 // c 是最新的
}); });
it('getLRU 返回最久未使用', () => { it('getLRU 返回最久未使用', () => {
const list = new LRUList(); const list = new LRUList();
const a = makePage(1); const a = makePage(1);
const b = makePage(2); const b = makePage(2);
list.moveToHead(a); list.moveToHead(a);
list.moveToHead(b); list.moveToHead(b);
expect(list.getLRU()!.pageId).toBe(1); expect(list.getLRU()!.pageId).toBe(1);
}); });
it('popLRU 移除并返回最久未使用', () => { it('popLRU 移除并返回最久未使用', () => {
const list = new LRUList(); const list = new LRUList();
const a = makePage(1); const a = makePage(1);
const b = makePage(2); const b = makePage(2);
list.moveToHead(a); list.moveToHead(a);
list.moveToHead(b); list.moveToHead(b);
const popped = list.popLRU(); const popped = list.popLRU();
expect(popped!.pageId).toBe(1); expect(popped!.pageId).toBe(1);
expect(list.size).toBe(1); expect(list.size).toBe(1);
}); });
it('remove — 从中间移除', () => { it('remove — 从中间移除', () => {
const list = new LRUList(); const list = new LRUList();
const a = makePage(1); const a = makePage(1);
const b = makePage(2); const b = makePage(2);
const c = makePage(3); const c = makePage(3);
list.moveToHead(a); list.moveToHead(a);
list.moveToHead(b); list.moveToHead(b);
list.moveToHead(c); list.moveToHead(c);
list.remove(b); list.remove(b);
expect(list.size).toBe(2); expect(list.size).toBe(2);
}); });
it('clear 清空', () => { it('clear 清空', () => {
const list = new LRUList(); const list = new LRUList();
list.moveToHead(makePage(1)); list.moveToHead(makePage(1));
list.moveToHead(makePage(2)); list.moveToHead(makePage(2));
list.clear(); list.clear();
expect(list.size).toBe(0); expect(list.size).toBe(0);
}); });
it('getAllPages 返回所有页面', () => { it('getAllPages 返回所有页面', () => {
const list = new LRUList(); const list = new LRUList();
list.moveToHead(makePage(1)); list.moveToHead(makePage(1));
list.moveToHead(makePage(2)); list.moveToHead(makePage(2));
expect(list.getAllPages()).toHaveLength(2); expect(list.getAllPages()).toHaveLength(2);
}); });
it('空 LRU getLRU 返回 null', () => { it('空 LRU getLRU 返回 null', () => {
const list = new LRUList(); const list = new LRUList();
expect(list.getLRU()).toBeNull(); expect(list.getLRU()).toBeNull();
}); });
it('空 LRU popLRU 返回 null', () => { it('空 LRU popLRU 返回 null', () => {
const list = new LRUList(); const list = new LRUList();
expect(list.popLRU()).toBeNull(); expect(list.popLRU()).toBeNull();
}); });
it('moveToHead 同元素不移重复', () => { it('moveToHead 同元素不移重复', () => {
const list = new LRUList(); const list = new LRUList();
const p = makePage(1); const p = makePage(1);
list.moveToHead(p); list.moveToHead(p);
list.moveToHead(p); list.moveToHead(p);
list.moveToHead(p); list.moveToHead(p);
expect(list.size).toBe(1); expect(list.size).toBe(1);
}); });
}); });
// =================================================================== // ===================================================================
// EvictionManager // EvictionManager
// =================================================================== // ===================================================================
describe('AriaEngine — EvictionManager', () => { describe('AriaEngine — EvictionManager', () => {
function makePage(id: number, dirty = false) { function makePage(id: number, dirty = false) {
const data = new ArrayBuffer(PAGE_SIZE); const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, id, PageType.DATA); initPageHeader(data, id, PageType.DATA);
return { pageId: id, type: PageType.DATA, data, dirty, pins: 0, prev: null, next: null, lastAccess: Date.now() }; return { pageId: id, type: PageType.DATA, data, dirty, pins: 0, prev: null, next: null, lastAccess: Date.now() };
} }
it('access 更新 LRU', async () => { it('access 更新 LRU', async () => {
let evicted = -1; let evicted = -1;
const em = new EvictionManager(3, async (p) => { evicted = p.pageId; }); const em = new EvictionManager(3, async (p) => { evicted = p.pageId; });
const p = makePage(1); const p = makePage(1);
em.add(p); em.add(p);
em.access(p); em.access(p);
expect(em.getSize()).toBe(1); expect(em.getSize()).toBe(1);
}); });
it('add 不超过容量不触发驱逐', async () => { it('add 不超过容量不触发驱逐', async () => {
const evictedPages: number[] = []; const evictedPages: number[] = [];
const em = new EvictionManager(4, async (p) => { evictedPages.push(p.pageId); }); const em = new EvictionManager(4, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1)); em.add(makePage(1));
em.add(makePage(2)); em.add(makePage(2));
em.add(makePage(3)); em.add(makePage(3));
await em.evictIfNeeded(1); await em.evictIfNeeded(1);
expect(evictedPages).toHaveLength(0); expect(evictedPages).toHaveLength(0);
expect(em.getSize()).toBe(3); expect(em.getSize()).toBe(3);
}); });
it('evictIfNeeded 超容量触发驱逐', async () => { it('evictIfNeeded 超容量触发驱逐', async () => {
const evictedPages: number[] = []; const evictedPages: number[] = [];
const em = new EvictionManager(2, async (p) => { evictedPages.push(p.pageId); }); const em = new EvictionManager(2, async (p) => { evictedPages.push(p.pageId); });
em.add(makePage(1)); em.add(makePage(1));
em.add(makePage(2)); em.add(makePage(2));
em.add(makePage(3)); // 超容量 em.add(makePage(3)); // 超容量
await em.evictIfNeeded(0); await em.evictIfNeeded(0);
// 驱逐后 size 应 <= 2 // 驱逐后 size 应 <= 2
expect(em.getSize()).toBeLessThanOrEqual(2); expect(em.getSize()).toBeLessThanOrEqual(2);
}); });
it('remove 减少 size', () => { it('remove 减少 size', () => {
const em = new EvictionManager(4, async () => {}); const em = new EvictionManager(4, async () => {});
const p = makePage(1); const p = makePage(1);
em.add(p); em.add(p);
em.add(makePage(2)); em.add(makePage(2));
em.remove(p); em.remove(p);
expect(em.getSize()).toBe(1); expect(em.getSize()).toBe(1);
}); });
it('getCapacity 返回配置容量', () => { it('getCapacity 返回配置容量', () => {
const em = new EvictionManager(128, async () => {}); const em = new EvictionManager(128, async () => {});
expect(em.getCapacity()).toBe(128); expect(em.getCapacity()).toBe(128);
}); });
it('clear 清空', () => { it('clear 清空', () => {
const em = new EvictionManager(4, async () => {}); const em = new EvictionManager(4, async () => {});
em.add(makePage(1)); em.add(makePage(1));
em.add(makePage(2)); em.add(makePage(2));
em.clear(); em.clear();
expect(em.getSize()).toBe(0); expect(em.getSize()).toBe(0);
}); });
it('脏页驱逐前调用 onEvict 回调', async () => { it('脏页驱逐前调用 onEvict 回调', async () => {
let flushed = 0; let flushed = 0;
const em = new EvictionManager(2, async (_p) => { flushed++; }); const em = new EvictionManager(2, async (_p) => { flushed++; });
em.add(makePage(1, true)); // dirty page em.add(makePage(1, true)); // dirty page
em.add(makePage(2)); em.add(makePage(2));
await em.evictIfNeeded(1); // need space → evict page 1 await em.evictIfNeeded(1); // need space → evict page 1
expect(flushed).toBeGreaterThanOrEqual(0); // May or may not evict expect(flushed).toBeGreaterThanOrEqual(0); // May or may not evict
}); });
}); });
// =================================================================== // ===================================================================
// BufferPool (with Mock PageIO) // BufferPool (with Mock PageIO)
// =================================================================== // ===================================================================
describe('AriaEngine — BufferPool', () => { describe('AriaEngine — BufferPool', () => {
class MockPageIO implements PageIO { class MockPageIO implements PageIO {
store = new Map<number, ArrayBuffer>(); store = new Map<number, ArrayBuffer>();
nextId = 1; nextId = 1;
reads = 0; reads = 0;
writes = 0; writes = 0;
async readPage(pageId: number) { this.reads++; return this.store.get(pageId) ?? null; } 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 writePage(pageId: number, data: ArrayBuffer) { this.writes++; this.store.set(pageId, data); }
async allocatePageId() { return this.nextId++; } async allocatePageId() { return this.nextId++; }
async freePageId(_pageId: number) {} async freePageId(_pageId: number) {}
} }
it('newPage 创建页面并 pin', async () => { it('newPage 创建页面并 pin', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
const page = await pool.newPage(); const page = await pool.newPage();
expect(page.pageId).toBe(1); expect(page.pageId).toBe(1);
expect(page.pins).toBe(1); expect(page.pins).toBe(1);
expect(page.type).toBe(PageType.DATA); expect(page.type).toBe(PageType.DATA);
pool.unpin(page); pool.unpin(page);
}); });
it('getPage — 池中已存在则 pin++', async () => { it('getPage — 池中已存在则 pin++', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
const p1 = await pool.newPage(); const p1 = await pool.newPage();
pool.unpin(p1); pool.unpin(p1);
const p2 = await pool.getPage(p1.pageId); const p2 = await pool.getPage(p1.pageId);
expect(p2!.pageId).toBe(p1.pageId); expect(p2!.pageId).toBe(p1.pageId);
expect(p2!.pins).toBe(1); expect(p2!.pins).toBe(1);
pool.unpin(p2!); pool.unpin(p2!);
}); });
it('markDirty + flushPage 写回', async () => { it('markDirty + flushPage 写回', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
const page = await pool.newPage(); const page = await pool.newPage();
new Uint8Array(page.data)[100] = 42; new Uint8Array(page.data)[100] = 42;
pool.markDirty(page); pool.markDirty(page);
pool.unpin(page); pool.unpin(page);
await pool.flushPage(page.pageId); await pool.flushPage(page.pageId);
expect(io.writes).toBeGreaterThan(0); expect(io.writes).toBeGreaterThan(0);
}); });
it('flushAll 刷新所有脏页', async () => { it('flushAll 刷新所有脏页', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 8); const pool = new BufferPool(io, 8);
const p1 = await pool.newPage(); const p1 = await pool.newPage();
const p2 = await pool.newPage(); const p2 = await pool.newPage();
pool.markDirty(p1); pool.markDirty(p1);
pool.markDirty(p2); pool.markDirty(p2);
pool.unpin(p1); pool.unpin(p1);
pool.unpin(p2); pool.unpin(p2);
await pool.flushAll(); await pool.flushAll();
expect(io.writes).toBe(2); expect(io.writes).toBe(2);
}); });
it('getCachedPageCount 返回缓存数', async () => { it('getCachedPageCount 返回缓存数', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
await pool.newPage(); await pool.newPage();
await pool.newPage(); await pool.newPage();
expect(pool.getCachedPageCount()).toBe(2); expect(pool.getCachedPageCount()).toBe(2);
}); });
it('getDirtyPageCount 返回脏页数', async () => { it('getDirtyPageCount 返回脏页数', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
const p = await pool.newPage(); const p = await pool.newPage();
pool.markDirty(p); pool.markDirty(p);
pool.unpin(p); pool.unpin(p);
expect(pool.getDirtyPageCount()).toBe(1); expect(pool.getDirtyPageCount()).toBe(1);
}); });
it('getCapacity 返回容量', async () => { it('getCapacity 返回容量', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 64); const pool = new BufferPool(io, 64);
expect(pool.getCapacity()).toBe(64); expect(pool.getCapacity()).toBe(64);
}); });
it('removePage 移除缓存', async () => { it('removePage 移除缓存', async () => {
const io = new MockPageIO(); const io = new MockPageIO();
const pool = new BufferPool(io, 4); const pool = new BufferPool(io, 4);
const p = await pool.newPage(); const p = await pool.newPage();
pool.unpin(p); pool.unpin(p);
pool.removePage(p.pageId); pool.removePage(p.pageId);
expect(pool.getCachedPageCount()).toBe(0); 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 * AriaEngine LZ4 + LSM Merge Iterator
* LZ4 compression:false * LZ4 compression:false
*/ */
import { compressLZ4, decompressLZ4 } from '../../src/engine/aria/compression/lz4'; import { compressLZ4, decompressLZ4 } from '../../src/engine/aria/compression/lz4';
import { MergeIterator, ArrayEntrySource } from '../../src/engine/aria/index/merge_iterator'; import { MergeIterator, ArrayEntrySource } from '../../src/engine/aria/index/merge_iterator';
// =================================================================== // ===================================================================
// LZ4 压缩 — 安全烟雾测试(不卡死) // LZ4 压缩 — 安全烟雾测试(不卡死)
// =================================================================== // ===================================================================
describe('AriaEngine — LZ4 Compression', () => { describe('AriaEngine — LZ4 Compression', () => {
it('短于 4 字节时原样返回', () => { it('短于 4 字节时压缩为纯字面量 token 且往返一致', () => {
const input = new Uint8Array([1, 2]); const input = new Uint8Array([1, 2]);
const compressed = compressLZ4(input); const compressed = compressLZ4(input);
expect(compressed).toBe(input); // token(lo=0) + 2 字节字面量
}); expect(compressed.byteLength).toBe(3);
expect(compressed[0]).toBe(0x20); // litLen=2, matchField=0
it('简单文本压缩不抛出异常且产生输出', () => { const restored = decompressLZ4(compressed, 2);
const input = new TextEncoder().encode('hello world hello world hello world'); expect(Array.from(restored)).toEqual([1, 2]);
const compressed = compressLZ4(input); });
expect(compressed).toBeInstanceOf(Uint8Array);
expect(compressed.byteLength).toBeGreaterThan(0); it('简单文本压缩不抛出异常且产生输出', () => {
}); const input = new TextEncoder().encode('hello world hello world hello world');
const compressed = compressLZ4(input);
it('重复数据有压缩效果', () => { expect(compressed).toBeInstanceOf(Uint8Array);
const pattern = 'ABCD'; expect(compressed.byteLength).toBeGreaterThan(0);
const repeated = pattern.repeat(100); });
const input = new TextEncoder().encode(repeated);
const compressed = compressLZ4(input); it('重复数据有压缩效果', () => {
expect(compressed.byteLength).toBeLessThan(input.byteLength); const pattern = 'ABCD';
}); const repeated = pattern.repeat(100);
const input = new TextEncoder().encode(repeated);
it('随机不可压缩数据不卡死', () => { const compressed = compressLZ4(input);
const input = new Uint8Array(256); expect(compressed.byteLength).toBeLessThan(input.byteLength);
for (let i = 0; i < 256; i++) input[i] = Math.floor(Math.random() * 256); });
const compressed = compressLZ4(input);
expect(compressed).toBeInstanceOf(Uint8Array); it('随机不可压缩数据不卡死', () => {
expect(compressed.byteLength).toBeGreaterThan(0); const input = new Uint8Array(256);
}); for (let i = 0; i < 256; i++) input[i] = Math.floor(Math.random() * 256);
const compressed = compressLZ4(input);
it('长文本压缩不卡死', () => { expect(compressed).toBeInstanceOf(Uint8Array);
const input = new TextEncoder().encode('The quick brown fox jumps over the lazy dog. '.repeat(10)); expect(compressed.byteLength).toBeGreaterThan(0);
const compressed = compressLZ4(input); });
expect(compressed).toBeTruthy();
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);
it('多种长度输入均不卡死', () => { expect(compressed).toBeTruthy();
for (const size of [10, 50, 100, 200, 500]) { expect(compressed.byteLength).toBeGreaterThan(0);
const input = new Uint8Array(size); });
for (let i = 0; i < size; i++) input[i] = i % 256;
const compressed = compressLZ4(input); it('多种长度输入均不卡死', () => {
expect(compressed.byteLength).toBeLessThanOrEqual(input.byteLength + 16); 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);
it('解压不抛出异常', () => { expect(compressed.byteLength).toBeLessThanOrEqual(input.byteLength + 16);
const input = new TextEncoder().encode('test data for decompression smoke test'); }
const compressed = compressLZ4(input); });
expect(() => decompressLZ4(compressed, input.byteLength)).not.toThrow();
}); 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', () => { // ---- v0.2.6 补强:压缩 → 解压 往返一致性 ----
it('单数据源归并', () => {
const mi = new MergeIterator(); it('往返一致:重复模式数据', () => {
mi.addSource(new ArrayEntrySource([ const input = new TextEncoder().encode('ABCD'.repeat(100));
['a', { v: 1 }], const compressed = compressLZ4(input);
['b', { v: 2 }], const restored = decompressLZ4(compressed, input.byteLength);
['c', { v: 3 }], expect(Array.from(restored)).toEqual(Array.from(input));
])); });
const result = mi.drain();
expect(result).toHaveLength(3); it('往返一致:自然文本数据', () => {
expect(result.map(([k]) => k)).toEqual(['a', 'b', 'c']); const input = new TextEncoder().encode(
}); 'The quick brown fox jumps over the lazy dog. '.repeat(10),
);
it('多数据源归并去重(保留最新)', () => { const compressed = compressLZ4(input);
const mi = new MergeIterator(); const restored = decompressLZ4(compressed, input.byteLength);
mi.addSource(new ArrayEntrySource([['a', { v: 'new' }], ['c', { v: 3 }]])); expect(Array.from(restored)).toEqual(Array.from(input));
mi.addSource(new ArrayEntrySource([['a', { v: 'old' }], ['b', { v: 2 }]])); });
const result = mi.drain();
expect(result).toHaveLength(3); it('往返一致:随机不可压缩数据', () => {
expect(result[0][0]).toBe('a'); const input = new Uint8Array(512);
expect(result[0][1].v).toBe('new'); for (let i = 0; i < 512; i++) input[i] = Math.floor(Math.random() * 256);
expect(result[1][0]).toBe('b'); const compressed = compressLZ4(input);
expect(result[2][0]).toBe('c'); const restored = decompressLZ4(compressed, input.byteLength);
}); expect(Array.from(restored)).toEqual(Array.from(input));
});
it('空数据源归并', () => {
const mi = new MergeIterator(); it('往返一致:多种长度与字节模式', () => {
mi.addSource(new ArrayEntrySource([])); for (const size of [4, 5, 15, 16, 17, 50, 100, 300, 1000]) {
const result = mi.drain(); const input = new Uint8Array(size);
expect(result).toHaveLength(0); 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);
it('大数量归并', () => { expect(Array.from(restored)).toEqual(Array.from(input));
const mi = new MergeIterator(); }
for (let s = 0; s < 5; s++) { });
const entries: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) { it('往返一致:恰好 15 字节字面量边界', () => {
entries.push([`src${s}-key-${String(i).padStart(3, '0')}`, { src: s, idx: i }]); // 字面量长度恰好 15(token 上限)时不应丢字节
} const input = new Uint8Array(15);
mi.addSource(new ArrayEntrySource(entries)); for (let i = 0; i < 15; i++) input[i] = i;
} const compressed = compressLZ4(input);
const result = mi.drain(); const restored = decompressLZ4(compressed, input.byteLength);
// 5 sources × 100 unique keys = 500 total (keys are unique per source) expect(Array.from(restored)).toEqual(Array.from(input));
expect(result).toHaveLength(500); });
});
it('往返一致:超过 15 字节的连续匹配', () => {
it('ArrayEntrySource — 迭代器用完返回 null', () => { const input = new TextEncoder().encode('X'.repeat(200) + 'Y' + 'X'.repeat(60));
const src = new ArrayEntrySource([['k', { v: 1 }]]); const compressed = compressLZ4(input);
expect(src.next()).not.toBeNull(); const restored = decompressLZ4(compressed, input.byteLength);
expect(src.next()).toBeNull(); expect(Array.from(restored)).toEqual(Array.from(input));
expect(src.next()).toBeNull(); });
}); });
it('ArrayEntrySource — reset 重置', () => { // ===================================================================
const src = new ArrayEntrySource([['k1', { v: 1 }], ['k2', { v: 2 }]]); // MergeIterator — 归并迭代器单元测试
src.next(); // ===================================================================
src.reset(); describe('AriaEngine — MergeIterator', () => {
const val = src.next(); it('单数据源归并', () => {
expect(val![0]).toBe('k1'); 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 * AriaEngine Bloom Filter + MemTable
*/ */
import { BloomFilter } from '../../src/engine/aria/index/bloom'; import { BloomFilter } from '../../src/engine/aria/index/bloom';
import { MemTable } from '../../src/engine/aria/index/memtable'; import { MemTable } from '../../src/engine/aria/index/memtable';
// =================================================================== // ===================================================================
// BloomFilter // BloomFilter
// =================================================================== // ===================================================================
describe('AriaEngine — BloomFilter', () => { describe('AriaEngine — BloomFilter', () => {
it('插入后 mayContain 返回 true', () => { it('插入后 mayContain 返回 true', () => {
const bf = new BloomFilter(100); const bf = new BloomFilter(100);
bf.insert('hello'); bf.insert('hello');
expect(bf.mayContain('hello')).toBe(true); expect(bf.mayContain('hello')).toBe(true);
}); });
it('未插入的 key mayContain 返回 false', () => { it('未插入的 key mayContain 返回 false', () => {
const bf = new BloomFilter(100); const bf = new BloomFilter(100);
bf.insert('hello'); bf.insert('hello');
expect(bf.mayContain('world')).toBe(false); expect(bf.mayContain('world')).toBe(false);
}); });
it('批量插入后所有 key 都判定存在', () => { it('批量插入后所有 key 都判定存在', () => {
const bf = new BloomFilter(500); const bf = new BloomFilter(500);
const keys: string[] = []; const keys: string[] = [];
for (let i = 0; i < 200; i++) { for (let i = 0; i < 200; i++) {
const k = `key-${i}`; const k = `key-${i}`;
keys.push(k); keys.push(k);
bf.insert(k); bf.insert(k);
} }
for (const k of keys) { for (const k of keys) {
expect(bf.mayContain(k)).toBe(true); expect(bf.mayContain(k)).toBe(true);
} }
}); });
it('False positive 率可控', () => { it('False positive 率可控', () => {
const n = 500; const n = 500;
const bf = new BloomFilter(n, 10); const bf = new BloomFilter(n, 10);
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
bf.insert(`present-${i}`); bf.insert(`present-${i}`);
} }
let fp = 0; let fp = 0;
for (let i = 0; i < 500; i++) { for (let i = 0; i < 500; i++) {
if (bf.mayContain(`absent-${i}`)) fp++; if (bf.mayContain(`absent-${i}`)) fp++;
} }
expect(fp).toBeLessThan(25); expect(fp).toBeLessThan(25);
}); });
it('getBitSize 返回正确位数', () => { it('getBitSize 返回正确位数', () => {
const bf = new BloomFilter(100, 10); const bf = new BloomFilter(100, 10);
expect(bf.getBitSize()).toBeGreaterThanOrEqual(64); expect(bf.getBitSize()).toBeGreaterThanOrEqual(64);
}); });
it('getInsertedCount 追踪插入数', () => { it('getInsertedCount 追踪插入数', () => {
const bf = new BloomFilter(100); const bf = new BloomFilter(100);
bf.insert('a'); bf.insert('a');
bf.insert('b'); bf.insert('b');
bf.insert('c'); bf.insert('c');
expect(bf.getInsertedCount()).toBe(3); expect(bf.getInsertedCount()).toBe(3);
}); });
it('getHashCount 返回哈希函数数量', () => { it('getHashCount 返回哈希函数数量', () => {
const bf = new BloomFilter(1000, 10); const bf = new BloomFilter(1000, 10);
expect(bf.getHashCount()).toBeGreaterThan(0); expect(bf.getHashCount()).toBeGreaterThan(0);
}); });
it('serialize + fromData 往返', () => { it('serialize + fromData 往返', () => {
const bf1 = new BloomFilter(100); const bf1 = new BloomFilter(100);
bf1.insert('a'); bf1.insert('a');
bf1.insert('b'); bf1.insert('b');
const data = bf1.serialize(); const data = bf1.serialize();
const bf2 = BloomFilter.fromData(data, bf1.getHashCount()); const bf2 = BloomFilter.fromData(data, bf1.getHashCount());
expect(bf2.mayContain('a')).toBe(true); expect(bf2.mayContain('a')).toBe(true);
expect(bf2.mayContain('b')).toBe(true); expect(bf2.mayContain('b')).toBe(true);
expect(bf2.mayContain('c')).toBe(false); expect(bf2.mayContain('c')).toBe(false);
}); });
it('空过滤器 mayContain 返回 false', () => { it('空过滤器 mayContain 返回 false', () => {
const bf = new BloomFilter(100); const bf = new BloomFilter(100);
expect(bf.mayContain('anything')).toBe(false); expect(bf.mayContain('anything')).toBe(false);
}); });
it('最少 1 个哈希函数', () => { it('最少 1 个哈希函数', () => {
const bf = new BloomFilter(10, 1); const bf = new BloomFilter(10, 1);
expect(bf.getHashCount()).toBeGreaterThanOrEqual(1); expect(bf.getHashCount()).toBeGreaterThanOrEqual(1);
}); });
}); });
// =================================================================== // ===================================================================
// MemTable // MemTable
// =================================================================== // ===================================================================
describe('AriaEngine — MemTable', () => { describe('AriaEngine — MemTable', () => {
let mt: MemTable; let mt: MemTable;
beforeEach(() => { beforeEach(() => {
mt = new MemTable(4 * 1024 * 1024); mt = new MemTable(4 * 1024 * 1024);
}); });
it('put + get 往返', () => { it('put + get 往返', () => {
mt.put('key1', { name: 'Alice', age: 30 }); mt.put('key1', { name: 'Alice', age: 30 });
const val = mt.get('key1'); const val = mt.get('key1');
expect(val).not.toBeNull(); expect(val).not.toBeNull();
expect(val!.name).toBe('Alice'); expect(val!.name).toBe('Alice');
}); });
it('get — 不存在的 key 返回 null', () => { it('get — 不存在的 key 返回 null', () => {
expect(mt.get('nonexistent')).toBeNull(); expect(mt.get('nonexistent')).toBeNull();
}); });
it('put 更新已存在的 key', () => { it('put 更新已存在的 key', () => {
mt.put('k', { v: 1 }); mt.put('k', { v: 1 });
mt.put('k', { v: 2 }); mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2); expect(mt.get('k')!.v).toBe(2);
}); });
it('delete 删除成功', () => { it('delete 删除成功', () => {
mt.put('k', { v: 1 }); mt.put('k', { v: 1 });
expect(mt.delete('k')).toBe(true); expect(mt.delete('k')).toBe(true);
expect(mt.get('k')).toBeNull(); expect(mt.get('k')).toBeNull();
}); });
it('delete — 不存在的 key 返回 false', () => { it('delete — 不存在的 key 返回 false', () => {
expect(mt.delete('ghost')).toBe(false); expect(mt.delete('ghost')).toBe(false);
}); });
it('getAllEntries 返回所有条目(有序)', () => { it('getAllEntries 返回所有条目(有序)', () => {
mt.put('c', { v: 3 }); mt.put('c', { v: 3 });
mt.put('a', { v: 1 }); mt.put('a', { v: 1 });
mt.put('b', { v: 2 }); mt.put('b', { v: 2 });
const entries = mt.getAllEntries(); const entries = mt.getAllEntries();
expect(entries).toHaveLength(3); expect(entries).toHaveLength(3);
expect(entries[0][0]).toBe('a'); expect(entries[0][0]).toBe('a');
expect(entries[1][0]).toBe('b'); expect(entries[1][0]).toBe('b');
expect(entries[2][0]).toBe('c'); expect(entries[2][0]).toBe('c');
}); });
it('rangeScan — 范围查询', () => { it('rangeScan — 范围查询', () => {
mt.put('a', { v: 1 }); mt.put('a', { v: 1 });
mt.put('b', { v: 2 }); mt.put('b', { v: 2 });
mt.put('c', { v: 3 }); mt.put('c', { v: 3 });
mt.put('d', { v: 4 }); mt.put('d', { v: 4 });
const results = mt.rangeScan('b', 'c'); const results = mt.rangeScan('b', 'c');
expect(results).toHaveLength(2); expect(results).toHaveLength(2);
expect(results[0][0]).toBe('b'); expect(results[0][0]).toBe('b');
expect(results[1][0]).toBe('c'); expect(results[1][0]).toBe('c');
}); });
it('rangeScan — 空结果', () => { it('rangeScan — 空结果', () => {
mt.put('a', { v: 1 }); mt.put('a', { v: 1 });
const results = mt.rangeScan('z', 'zz'); const results = mt.rangeScan('z', 'zz');
expect(results).toHaveLength(0); expect(results).toHaveLength(0);
}); });
it('getEntryCount 正确计数', () => { it('getEntryCount 正确计数', () => {
expect(mt.getEntryCount()).toBe(0); expect(mt.getEntryCount()).toBe(0);
mt.put('a', { v: 1 }); mt.put('a', { v: 1 });
mt.put('b', { v: 2 }); mt.put('b', { v: 2 });
expect(mt.getEntryCount()).toBe(2); expect(mt.getEntryCount()).toBe(2);
mt.delete('a'); mt.delete('a');
expect(mt.getEntryCount()).toBe(1); expect(mt.getEntryCount()).toBe(1);
}); });
it('contains 检查存在性', () => { it('contains 检查存在性', () => {
mt.put('x', { v: 1 }); mt.put('x', { v: 1 });
expect(mt.contains('x')).toBe(true); expect(mt.contains('x')).toBe(true);
expect(mt.contains('y')).toBe(false); expect(mt.contains('y')).toBe(false);
}); });
it('shouldFlush — 未达阈值返回 false', () => { it('shouldFlush — 未达阈值返回 false', () => {
expect(mt.shouldFlush()).toBe(false); expect(mt.shouldFlush()).toBe(false);
}); });
it('clear 清空所有数据', () => { it('clear 清空所有数据', () => {
mt.put('a', { v: 1 }); mt.put('a', { v: 1 });
mt.put('b', { v: 2 }); mt.put('b', { v: 2 });
mt.clear(); mt.clear();
expect(mt.getEntryCount()).toBe(0); expect(mt.getEntryCount()).toBe(0);
expect(mt.get('a')).toBeNull(); expect(mt.get('a')).toBeNull();
}); });
it('getEstimatedSize 返回合理估计值', () => { it('getEstimatedSize 返回合理估计值', () => {
expect(mt.getEstimatedSize()).toBe(0); expect(mt.getEstimatedSize()).toBe(0);
mt.put('hello', { name: 'world', count: 42 }); mt.put('hello', { name: 'world', count: 42 });
expect(mt.getEstimatedSize()).toBeGreaterThan(0); expect(mt.getEstimatedSize()).toBeGreaterThan(0);
}); });
it('大量数据插入保持有序', () => { it('大量数据插入保持有序', () => {
const count = 100; const count = 100;
for (let i = count - 1; i >= 0; i--) { for (let i = count - 1; i >= 0; i--) {
mt.put(`key-${String(i).padStart(3, '0')}`, { idx: i }); mt.put(`key-${String(i).padStart(3, '0')}`, { idx: i });
} }
const entries = mt.getAllEntries(); const entries = mt.getAllEntries();
expect(entries).toHaveLength(count); expect(entries).toHaveLength(count);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
expect(entries[i][1].idx).toBe(i); expect(entries[i][1].idx).toBe(i);
} }
}); });
it('删除后重新插入', () => { it('删除后重新插入', () => {
mt.put('k', { v: 1 }); mt.put('k', { v: 1 });
mt.delete('k'); mt.delete('k');
mt.put('k', { v: 2 }); mt.put('k', { v: 2 });
expect(mt.get('k')!.v).toBe(2); expect(mt.get('k')!.v).toBe(2);
}); });
it('范围扫描包含边界', () => { it('范围扫描包含边界', () => {
mt.put('aa', { v: 1 }); mt.put('aa', { v: 1 });
mt.put('ab', { v: 2 }); mt.put('ab', { v: 2 });
mt.put('ac', { v: 3 }); mt.put('ac', { v: 3 });
const results = mt.rangeScan('aa', 'ab'); const results = mt.rangeScan('aa', 'ab');
expect(results).toHaveLength(2); expect(results).toHaveLength(2);
expect(results[0][0]).toBe('aa'); expect(results[0][0]).toBe('aa');
expect(results[1][0]).toBe('ab'); expect(results[1][0]).toBe('ab');
}); });
}); });
+337 -337
View File
@@ -1,337 +1,337 @@
/** /**
* AriaEngine Page * AriaEngine Page
* 覆盖: PageHeader / Slot / Tuple + PageFormat * 覆盖: PageHeader / Slot / Tuple + PageFormat
*/ */
import { import {
PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE, PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE,
} from '../../src/engine/aria/types'; } from '../../src/engine/aria/types';
import { import {
encodePageHeader, decodePageHeader, initPageHeader, getPageType, encodePageHeader, decodePageHeader, initPageHeader, getPageType,
getSlotCount, getFreeStart, setFreeStart, setFreeEnd, getSlotCount, getFreeStart, setFreeStart, setFreeEnd,
} from '../../src/engine/aria/page/header'; } from '../../src/engine/aria/page/header';
import { import {
getSlotEntry, setSlotEntry, getSlotDirectorySize, getSlotEntry, setSlotEntry, getSlotDirectorySize,
getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot, getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot,
} from '../../src/engine/aria/page/slot'; } from '../../src/engine/aria/page/slot';
import { import {
encodeTuple, decodeTuple, getColumnEncodingMap, encodeTuple, decodeTuple, getColumnEncodingMap,
} from '../../src/engine/aria/page/tuple'; } from '../../src/engine/aria/page/tuple';
import { ColumnEncoding } from '../../src/engine/aria/types'; import { ColumnEncoding } from '../../src/engine/aria/types';
import { import {
createPage, pageFromBuffer, pageInsertRow, pageReadRow, createPage, pageFromBuffer, pageInsertRow, pageReadRow,
pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum, pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum,
} from '../../src/engine/aria/page/format'; } from '../../src/engine/aria/page/format';
// =================================================================== // ===================================================================
// PageHeader // PageHeader
// =================================================================== // ===================================================================
describe('AriaEngine Page — Header', () => { describe('AriaEngine Page — Header', () => {
let buf: ArrayBuffer; let buf: ArrayBuffer;
beforeEach(() => { beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE); buf = new ArrayBuffer(PAGE_SIZE);
}); });
it('initPageHeader 初始化头部字段', () => { it('initPageHeader 初始化头部字段', () => {
initPageHeader(buf, 42, PageType.DATA); initPageHeader(buf, 42, PageType.DATA);
const h = decodePageHeader(buf); const h = decodePageHeader(buf);
expect(h.pageId).toBe(42); expect(h.pageId).toBe(42);
expect(h.type).toBe(PageType.DATA); expect(h.type).toBe(PageType.DATA);
expect(h.slotCount).toBe(0); expect(h.slotCount).toBe(0);
expect(h.freeStart).toBe(PAGE_HEADER_SIZE); expect(h.freeStart).toBe(PAGE_HEADER_SIZE);
expect(h.freeEnd).toBe(PAGE_SIZE); expect(h.freeEnd).toBe(PAGE_SIZE);
}); });
it('initPageHeader — INDEX 类型页面', () => { it('initPageHeader — INDEX 类型页面', () => {
initPageHeader(buf, 99, PageType.INDEX); initPageHeader(buf, 99, PageType.INDEX);
expect(getPageType(buf)).toBe(PageType.INDEX); expect(getPageType(buf)).toBe(PageType.INDEX);
}); });
it('encodePageHeader + decodePageHeader 往返一致', () => { it('encodePageHeader + decodePageHeader 往返一致', () => {
const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef }; const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef };
encodePageHeader(header, buf); encodePageHeader(header, buf);
const decoded = decodePageHeader(buf); const decoded = decodePageHeader(buf);
expect(decoded.pageId).toBe(7); expect(decoded.pageId).toBe(7);
expect(decoded.type).toBe(PageType.META); expect(decoded.type).toBe(PageType.META);
expect(decoded.freeStart).toBe(32); expect(decoded.freeStart).toBe(32);
expect(decoded.freeEnd).toBe(4000); expect(decoded.freeEnd).toBe(4000);
expect(decoded.slotCount).toBe(5); expect(decoded.slotCount).toBe(5);
}); });
it('不同 pageId 正确编解码', () => { it('不同 pageId 正确编解码', () => {
for (const id of [0, 1, 255, 65535, 0xffffffff]) { for (const id of [0, 1, 255, 65535, 0xffffffff]) {
initPageHeader(buf, id, PageType.DATA); initPageHeader(buf, id, PageType.DATA);
expect(decodePageHeader(buf).pageId).toBe(id >>> 0); expect(decodePageHeader(buf).pageId).toBe(id >>> 0);
} }
}); });
it('setFreeStart / setFreeEnd 修改字段', () => { it('setFreeStart / setFreeEnd 修改字段', () => {
initPageHeader(buf, 1, PageType.DATA); initPageHeader(buf, 1, PageType.DATA);
setFreeStart(buf, 100); setFreeStart(buf, 100);
setFreeEnd(buf, 3000); setFreeEnd(buf, 3000);
expect(getFreeStart(buf)).toBe(100); expect(getFreeStart(buf)).toBe(100);
expect(decodePageHeader(buf).freeEnd).toBe(3000); expect(decodePageHeader(buf).freeEnd).toBe(3000);
}); });
}); });
// =================================================================== // ===================================================================
// Slot Directory // Slot Directory
// =================================================================== // ===================================================================
describe('AriaEngine Page — Slot', () => { describe('AriaEngine Page — Slot', () => {
let buf: ArrayBuffer; let buf: ArrayBuffer;
beforeEach(() => { beforeEach(() => {
buf = new ArrayBuffer(PAGE_SIZE); buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, 1, PageType.DATA); initPageHeader(buf, 1, PageType.DATA);
}); });
it('getSlotEntry — 空页面 slotCount 为 0', () => { it('getSlotEntry — 空页面 slotCount 为 0', () => {
expect(getSlotCount(buf)).toBe(0); expect(getSlotCount(buf)).toBe(0);
}); });
it('setSlotEntry + getSlotEntry 往返', () => { it('setSlotEntry + getSlotEntry 往返', () => {
// 手动写一个 slot(不通过 allocateSlot // 手动写一个 slot(不通过 allocateSlot
new DataView(buf).setUint16(9, 1, false); // slotCount = 1 new DataView(buf).setUint16(9, 1, false); // slotCount = 1
setSlotEntry(buf, 0, { offset: 1000, length: 50 }); setSlotEntry(buf, 0, { offset: 1000, length: 50 });
const entry = getSlotEntry(buf, 0); const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(1000); expect(entry.offset).toBe(1000);
expect(entry.length).toBe(50); expect(entry.length).toBe(50);
}); });
it('getSlotDirectorySize 计算正确', () => { it('getSlotDirectorySize 计算正确', () => {
expect(getSlotDirectorySize(0)).toBe(0); expect(getSlotDirectorySize(0)).toBe(0);
expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE); expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE);
expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE); expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE);
}); });
it('getFreeSpace — 空页面有最大空闲空间', () => { it('getFreeSpace — 空页面有最大空闲空间', () => {
const free = getFreeSpace(buf); const free = getFreeSpace(buf);
expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE); expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE);
}); });
it('hasEnoughSpace — 小数据返回 true', () => { it('hasEnoughSpace — 小数据返回 true', () => {
expect(hasEnoughSpace(buf, 100)).toBe(true); expect(hasEnoughSpace(buf, 100)).toBe(true);
}); });
it('hasEnoughSpace — 超大数据返回 false', () => { it('hasEnoughSpace — 超大数据返回 false', () => {
expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false); expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false);
}); });
it('allocateSlot 分配并写入数据', () => { it('allocateSlot 分配并写入数据', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]); const data = new Uint8Array([1, 2, 3, 4, 5]);
const idx = allocateSlot(buf, data); const idx = allocateSlot(buf, data);
expect(idx).toBe(0); expect(idx).toBe(0);
expect(getSlotCount(buf)).toBe(1); expect(getSlotCount(buf)).toBe(1);
const readBack = readSlotData(buf, 0); const readBack = readSlotData(buf, 0);
expect(readBack).not.toBeNull(); expect(readBack).not.toBeNull();
expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]); expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]);
}); });
it('allocateSlot 多次分配', () => { it('allocateSlot 多次分配', () => {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const data = new Uint8Array([i, i + 1]); const data = new Uint8Array([i, i + 1]);
const idx = allocateSlot(buf, data); const idx = allocateSlot(buf, data);
expect(idx).toBe(i); expect(idx).toBe(i);
} }
expect(getSlotCount(buf)).toBe(10); expect(getSlotCount(buf)).toBe(10);
// 验证每个 slot 数据正确 // 验证每个 slot 数据正确
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const data = readSlotData(buf, i); const data = readSlotData(buf, i);
expect(Array.from(data!)).toEqual([i, i + 1]); expect(Array.from(data!)).toEqual([i, i + 1]);
} }
}); });
it('freeSlot 标记为删除', () => { it('freeSlot 标记为删除', () => {
const data = new Uint8Array([10, 20, 30]); const data = new Uint8Array([10, 20, 30]);
allocateSlot(buf, data); allocateSlot(buf, data);
freeSlot(buf, 0); freeSlot(buf, 0);
const entry = getSlotEntry(buf, 0); const entry = getSlotEntry(buf, 0);
expect(entry.offset).toBe(0); expect(entry.offset).toBe(0);
expect(entry.length).toBe(0); expect(entry.length).toBe(0);
}); });
it('readSlotData — 已删除 slot 返回 null', () => { it('readSlotData — 已删除 slot 返回 null', () => {
allocateSlot(buf, new Uint8Array([1])); allocateSlot(buf, new Uint8Array([1]));
freeSlot(buf, 0); freeSlot(buf, 0);
expect(readSlotData(buf, 0)).toBeNull(); expect(readSlotData(buf, 0)).toBeNull();
}); });
it('allocateSlot — 空间不足返回 -1', () => { it('allocateSlot — 空间不足返回 -1', () => {
// 填满页面 // 填满页面
const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE); const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE);
const idx1 = allocateSlot(buf, big); const idx1 = allocateSlot(buf, big);
expect(idx1).toBe(0); expect(idx1).toBe(0);
const idx2 = allocateSlot(buf, new Uint8Array([1])); const idx2 = allocateSlot(buf, new Uint8Array([1]));
expect(idx2).toBe(-1); expect(idx2).toBe(-1);
}); });
}); });
// =================================================================== // ===================================================================
// Tuple Codec // Tuple Codec
// =================================================================== // ===================================================================
describe('AriaEngine Page — Tuple', () => { describe('AriaEngine Page — Tuple', () => {
const colOrder = ['id', 'name', 'age', 'active', 'data']; const colOrder = ['id', 'name', 'age', 'active', 'data'];
const colTypes: Record<string, string> = { const colTypes: Record<string, string> = {
id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json', id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json',
}; };
it('encodeTuple + decodeTuple 完整往返', () => { it('encodeTuple + decodeTuple 完整往返', () => {
const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } }; const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } };
const encoded = encodeTuple(row, colOrder, colTypes); const encoded = encodeTuple(row, colOrder, colTypes);
expect(encoded.byteLength).toBeGreaterThan(0); expect(encoded.byteLength).toBeGreaterThan(0);
const decoded = decodeTuple(encoded, colOrder, colTypes); const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded).not.toBeNull(); expect(decoded).not.toBeNull();
expect(decoded!.id).toBe('1'); expect(decoded!.id).toBe('1');
expect(decoded!.name).toBe('Alice'); expect(decoded!.name).toBe('Alice');
expect(decoded!.age).toBe(30); expect(decoded!.age).toBe(30);
expect(decoded!.active).toBe(true); expect(decoded!.active).toBe(true);
expect(decoded!.data).toEqual({ x: 1 }); expect(decoded!.data).toEqual({ x: 1 });
}); });
it('encodeTuple — null 值正确处理', () => { it('encodeTuple — null 值正确处理', () => {
const row = { id: '2', name: null, age: 25, active: null, data: null }; const row = { id: '2', name: null, age: 25, active: null, data: null };
const encoded = encodeTuple(row, colOrder, colTypes); const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes); const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.name).toBeNull(); expect(decoded!.name).toBeNull();
expect(decoded!.active).toBeNull(); expect(decoded!.active).toBeNull();
expect(decoded!.data).toBeNull(); expect(decoded!.data).toBeNull();
}); });
it('encodeTuple — undefined 值按 null 处理', () => { it('encodeTuple — undefined 值按 null 处理', () => {
const row = { id: '3', age: 30 } as any; const row = { id: '3', age: 30 } as any;
const encoded = encodeTuple(row, colOrder, colTypes); const encoded = encodeTuple(row, colOrder, colTypes);
const decoded = decodeTuple(encoded, colOrder, colTypes); const decoded = decodeTuple(encoded, colOrder, colTypes);
expect(decoded!.id).toBe('3'); expect(decoded!.id).toBe('3');
expect(decoded!.name).toBeNull(); expect(decoded!.name).toBeNull();
}); });
it('encodeTuple — date 类型', () => { it('encodeTuple — date 类型', () => {
const order = ['ts']; const order = ['ts'];
const types = { ts: 'date' }; const types = { ts: 'date' };
const row = { ts: '2024-01-15T00:00:00.000Z' }; const row = { ts: '2024-01-15T00:00:00.000Z' };
const encoded = encodeTuple(row, order, types); const encoded = encodeTuple(row, order, types);
const decoded = decodeTuple(encoded, order, types); const decoded = decodeTuple(encoded, order, types);
expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z'); expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z');
}); });
it('encodeTuple — boolean false', () => { it('encodeTuple — boolean false', () => {
const order = ['flag']; const order = ['flag'];
const types = { flag: 'boolean' }; const types = { flag: 'boolean' };
const encoded = encodeTuple({ flag: false }, order, types); const encoded = encodeTuple({ flag: false }, order, types);
const decoded = decodeTuple(encoded, order, types); const decoded = decodeTuple(encoded, order, types);
expect(decoded!.flag).toBe(false); expect(decoded!.flag).toBe(false);
}); });
it('encodeTuple — 负数', () => { it('encodeTuple — 负数', () => {
const order = ['val']; const order = ['val'];
const types = { val: 'number' }; const types = { val: 'number' };
const encoded = encodeTuple({ val: -42.5 }, order, types); const encoded = encodeTuple({ val: -42.5 }, order, types);
const decoded = decodeTuple(encoded, order, types); const decoded = decodeTuple(encoded, order, types);
expect(decoded!.val).toBe(-42.5); expect(decoded!.val).toBe(-42.5);
}); });
it('encodeTuple — 空字符串', () => { it('encodeTuple — 空字符串', () => {
const order = ['s']; const order = ['s'];
const types = { s: 'string' }; const types = { s: 'string' };
const encoded = encodeTuple({ s: '' }, order, types); const encoded = encodeTuple({ s: '' }, order, types);
const decoded = decodeTuple(encoded, order, types); const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe(''); expect(decoded!.s).toBe('');
}); });
it('encodeTuple — 长字符串', () => { it('encodeTuple — 长字符串', () => {
const order = ['s']; const order = ['s'];
const types = { s: 'string' }; const types = { s: 'string' };
const long = 'x'.repeat(10000); const long = 'x'.repeat(10000);
const encoded = encodeTuple({ s: long }, order, types); const encoded = encodeTuple({ s: long }, order, types);
const decoded = decodeTuple(encoded, order, types); const decoded = decodeTuple(encoded, order, types);
expect(decoded!.s).toBe(long); expect(decoded!.s).toBe(long);
}); });
it('getColumnEncodingMap 返回正确映射', () => { it('getColumnEncodingMap 返回正确映射', () => {
const map = getColumnEncodingMap(colOrder, colTypes); const map = getColumnEncodingMap(colOrder, colTypes);
expect(map.get('id')).toBe(ColumnEncoding.STRING); expect(map.get('id')).toBe(ColumnEncoding.STRING);
expect(map.get('age')).toBe(ColumnEncoding.NUMBER); expect(map.get('age')).toBe(ColumnEncoding.NUMBER);
expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN); expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN);
expect(map.get('data')).toBe(ColumnEncoding.JSON); expect(map.get('data')).toBe(ColumnEncoding.JSON);
}); });
it('decodeTuple — 损坏数据返回 null', () => { it('decodeTuple — 损坏数据返回 null', () => {
const broken = new Uint8Array([0xff, 0xff, 0xff]); const broken = new Uint8Array([0xff, 0xff, 0xff]);
expect(decodeTuple(broken, colOrder, colTypes)).toBeNull(); expect(decodeTuple(broken, colOrder, colTypes)).toBeNull();
}); });
}); });
// =================================================================== // ===================================================================
// Page Format 整合 // Page Format 整合
// =================================================================== // ===================================================================
describe('AriaEngine Page — Format', () => { describe('AriaEngine Page — Format', () => {
const colOrder = ['id', 'name']; const colOrder = ['id', 'name'];
const colTypes: Record<string, string> = { id: 'string', name: 'string' }; const colTypes: Record<string, string> = { id: 'string', name: 'string' };
it('createPage 创建合法页面', () => { it('createPage 创建合法页面', () => {
const page = createPage(100, PageType.DATA); const page = createPage(100, PageType.DATA);
expect(page.pageId).toBe(100); expect(page.pageId).toBe(100);
expect(page.type).toBe(PageType.DATA); expect(page.type).toBe(PageType.DATA);
expect(page.pins).toBe(0); expect(page.pins).toBe(0);
expect(page.dirty).toBe(true); expect(page.dirty).toBe(true);
}); });
it('pageInsertRow + pageReadRow 往返', () => { it('pageInsertRow + pageReadRow 往返', () => {
const page = createPage(1, PageType.DATA); const page = createPage(1, PageType.DATA);
const row = { id: 'u1', name: 'Test' }; const row = { id: 'u1', name: 'Test' };
const idx = pageInsertRow(page, row, colOrder, colTypes); const idx = pageInsertRow(page, row, colOrder, colTypes);
expect(idx).toBe(0); expect(idx).toBe(0);
const read = pageReadRow(page, 0, colOrder, colTypes); const read = pageReadRow(page, 0, colOrder, colTypes);
expect(read).not.toBeNull(); expect(read).not.toBeNull();
expect(read!.id).toBe('u1'); expect(read!.id).toBe('u1');
expect(read!.name).toBe('Test'); expect(read!.name).toBe('Test');
}); });
it('pageInsertRow — 多次插入', () => { it('pageInsertRow — 多次插入', () => {
const page = createPage(1, PageType.DATA); const page = createPage(1, PageType.DATA);
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes); const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes);
expect(idx).toBe(i); expect(idx).toBe(i);
} }
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
const row = pageReadRow(page, i, colOrder, colTypes); const row = pageReadRow(page, i, colOrder, colTypes);
expect(row!.id).toBe(`${i}`); expect(row!.id).toBe(`${i}`);
} }
}); });
it('pageDeleteRow 标记删除', () => { it('pageDeleteRow 标记删除', () => {
const page = createPage(1, PageType.DATA); const page = createPage(1, PageType.DATA);
pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes); pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes);
pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes); pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes);
pageDeleteRow(page, 0); pageDeleteRow(page, 0);
expect(page.dirty).toBe(true); expect(page.dirty).toBe(true);
// 已删除的行读取失败 // 已删除的行读取失败
const row = pageReadRow(page, 0, colOrder, colTypes); const row = pageReadRow(page, 0, colOrder, colTypes);
expect(row).toBeNull(); expect(row).toBeNull();
// 未删除的行仍然可读 // 未删除的行仍然可读
const row2 = pageReadRow(page, 1, colOrder, colTypes); const row2 = pageReadRow(page, 1, colOrder, colTypes);
expect(row2!.id).toBe('2'); expect(row2!.id).toBe('2');
}); });
it('pageFromBuffer 从 ArrayBuffer 恢复', () => { it('pageFromBuffer 从 ArrayBuffer 恢复', () => {
const page = createPage(5, PageType.INDEX); const page = createPage(5, PageType.INDEX);
const restored = pageFromBuffer(5, page.data); const restored = pageFromBuffer(5, page.data);
expect(restored.pageId).toBe(5); expect(restored.pageId).toBe(5);
expect(restored.type).toBe(PageType.INDEX); expect(restored.type).toBe(PageType.INDEX);
expect(restored.dirty).toBe(false); expect(restored.dirty).toBe(false);
}); });
it('computeChecksum + verifyChecksum', () => { it('computeChecksum + verifyChecksum', () => {
const page = createPage(1, PageType.DATA); const page = createPage(1, PageType.DATA);
updateChecksum(page); updateChecksum(page);
expect(verifyChecksum(page)).toBe(true); expect(verifyChecksum(page)).toBe(true);
// 修改页面 → 校验和失效 // 修改页面 → 校验和失效
new Uint8Array(page.data)[100] = 0xff; new Uint8Array(page.data)[100] = 0xff;
expect(verifyChecksum(page)).toBe(false); expect(verifyChecksum(page)).toBe(false);
}); });
}); });
+129 -129
View File
@@ -1,129 +1,129 @@
/** /**
* AriaEngine SSTable Builder + Reader * AriaEngine SSTable Builder + Reader
*/ */
import { SSTableBuilder } from '../../src/engine/aria/index/sstable_builder'; import { SSTableBuilder } from '../../src/engine/aria/index/sstable_builder';
import { SSTableReader } from '../../src/engine/aria/index/sstable'; import { SSTableReader } from '../../src/engine/aria/index/sstable';
import type { SSTableMeta } from '../../src/engine/aria/types'; import type { SSTableMeta } from '../../src/engine/aria/types';
// =================================================================== // ===================================================================
// SSTable Builder + Reader // SSTable Builder + Reader
// =================================================================== // ===================================================================
describe('AriaEngine — SSTable Builder + Reader', () => { describe('AriaEngine — SSTable Builder + Reader', () => {
const makeMeta = (data: Uint8Array): SSTableMeta => ({ const makeMeta = (data: Uint8Array): SSTableMeta => ({
id: 1, level: 0, minKey: '', maxKey: '\uffff', id: 1, level: 0, minKey: '', maxKey: '\uffff',
blockCount: 1, totalSize: data.byteLength, bloomData: null, blockCount: 1, totalSize: data.byteLength, bloomData: null,
}); });
it('构建单条目 SSTable 并精确读取', () => { it('构建单条目 SSTable 并精确读取', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
builder.add('key1', { name: 'Alice', age: 30 }); builder.add('key1', { name: 'Alice', age: 30 });
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const result = reader.get('key1'); const result = reader.get('key1');
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result!.name).toBe('Alice'); expect(result!.name).toBe('Alice');
expect(result!.age).toBe(30); expect(result!.age).toBe(30);
}); });
it('构建多条 SSTable 并全部读取', () => { it('构建多条 SSTable 并全部读取', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
const items: [string, Record<string, unknown>][] = []; const items: [string, Record<string, unknown>][] = [];
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
const key = `user-${String(i).padStart(3, '0')}`; const key = `user-${String(i).padStart(3, '0')}`;
const value = { idx: i, name: `User${i}` }; const value = { idx: i, name: `User${i}` };
items.push([key, value]); items.push([key, value]);
builder.add(key, value); builder.add(key, value);
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
for (const [key, value] of items) { for (const [key, value] of items) {
const result = reader.get(key); const result = reader.get(key);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result!.idx).toBe(value.idx); expect(result!.idx).toBe(value.idx);
} }
}); });
it('get — 不存在的 key 返回 null', () => { it('get — 不存在的 key 返回 null', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 }); builder.add('a', { v: 1 });
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('nonexistent')).toBeNull(); expect(reader.get('nonexistent')).toBeNull();
}); });
it('rangeScan — 范围查询', () => { it('rangeScan — 范围查询', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
builder.add(`k-${String(i).padStart(2, '0')}`, { v: i }); builder.add(`k-${String(i).padStart(2, '0')}`, { v: i });
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const results: [string, Record<string, unknown>][] = []; const results: [string, Record<string, unknown>][] = [];
reader.rangeScan('k-05', 'k-10', (k, v) => results.push([k, v])); reader.rangeScan('k-05', 'k-10', (k, v) => results.push([k, v]));
expect(results).toHaveLength(6); expect(results).toHaveLength(6);
expect(results[0][0]).toBe('k-05'); expect(results[0][0]).toBe('k-05');
expect(results[results.length - 1][0]).toBe('k-10'); expect(results[results.length - 1][0]).toBe('k-10');
}); });
it('scanAll — 遍历所有条目', () => { it('scanAll — 遍历所有条目', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
const count = 50; const count = 50;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
builder.add(`item-${i}`, { idx: i }); builder.add(`item-${i}`, { idx: i });
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
const items: [string, Record<string, unknown>][] = []; const items: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => items.push([k, v])); reader.scanAll((k, v) => items.push([k, v]));
expect(items).toHaveLength(count); expect(items).toHaveLength(count);
}); });
it('getIndexBlockCount 返回索引块数', () => { it('getIndexBlockCount 返回索引块数', () => {
const builder = new SSTableBuilder(256); // 小 block size 触发多个 block const builder = new SSTableBuilder(256); // 小 block size 触发多个 block
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
builder.add(`k-${i}`, { data: 'x'.repeat(50) }); builder.add(`k-${i}`, { data: 'x'.repeat(50) });
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.getIndexBlockCount()).toBeGreaterThanOrEqual(1); expect(reader.getIndexBlockCount()).toBeGreaterThanOrEqual(1);
}); });
it('边界 — 空 SSTable 不抛异常', () => { it('边界 — 空 SSTable 不抛异常', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('any')).toBeNull(); expect(reader.get('any')).toBeNull();
const results: [string, Record<string, unknown>][] = []; const results: [string, Record<string, unknown>][] = [];
reader.scanAll((k, v) => results.push([k, v])); reader.scanAll((k, v) => results.push([k, v]));
expect(results).toHaveLength(0); expect(results).toHaveLength(0);
}); });
it('带特殊字符的 key', () => { it('带特殊字符的 key', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
// 必须按键排序添加(按 ASCII 排序:空格 < 短横 < 点号) // 必须按键排序添加(按 ASCII 排序:空格 < 短横 < 点号)
builder.add('key with space', { v: 3 }); builder.add('key with space', { v: 3 });
builder.add('key-with-dash', { v: 1 }); builder.add('key-with-dash', { v: 1 });
builder.add('key.with.dot', { v: 2 }); builder.add('key.with.dot', { v: 2 });
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const reader = new SSTableReader(sstableData, makeMeta(sstableData)); const reader = new SSTableReader(sstableData, makeMeta(sstableData));
expect(reader.get('key-with-dash')!.v).toBe(1); expect(reader.get('key-with-dash')!.v).toBe(1);
expect(reader.get('key.with.dot')!.v).toBe(2); expect(reader.get('key.with.dot')!.v).toBe(2);
expect(reader.get('key with space')!.v).toBe(3); expect(reader.get('key with space')!.v).toBe(3);
}); });
it('getEntryCount 返回正确条目数', () => { it('getEntryCount 返回正确条目数', () => {
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
builder.add('a', { v: 1 }); builder.add('a', { v: 1 });
builder.add('b', { v: 2 }); builder.add('b', { v: 2 });
builder.add('c', { v: 3 }); builder.add('c', { v: 3 });
expect(builder.getEntryCount()).toBe(3); expect(builder.getEntryCount()).toBe(3);
}); });
}); });
+289 -289
View File
@@ -1,289 +1,289 @@
/** /**
* AriaEngine WAL + MVCC * AriaEngine WAL + MVCC
*/ */
import { WAL, type WALStore } from '../../src/engine/aria/wal/log'; import { WAL, type WALStore } from '../../src/engine/aria/wal/log';
import { WALRecordType, type WALRecord } from '../../src/engine/aria/types'; import { WALRecordType, type WALRecord } from '../../src/engine/aria/types';
import { CheckpointManager, type Flushable } from '../../src/engine/aria/wal/checkpoint'; import { CheckpointManager, type Flushable } from '../../src/engine/aria/wal/checkpoint';
import { MVCCManager } from '../../src/engine/aria/transaction/mvcc'; import { MVCCManager } from '../../src/engine/aria/transaction/mvcc';
// =================================================================== // ===================================================================
// WAL 存储 Mock // WAL 存储 Mock
// =================================================================== // ===================================================================
class MockWALStore implements WALStore { class MockWALStore implements WALStore {
chunks: Uint8Array[] = []; chunks: Uint8Array[] = [];
async append(data: Uint8Array) { this.chunks.push(data); } async append(data: Uint8Array) { this.chunks.push(data); }
async readAll(): Promise<Uint8Array> { async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0); const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total); const combined = new Uint8Array(total);
let off = 0; let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; } for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined; return combined;
} }
async truncate() { this.chunks = []; } async truncate() { this.chunks = []; }
async exists() { return this.chunks.length > 0; } async exists() { return this.chunks.length > 0; }
} }
// =================================================================== // ===================================================================
// WAL 测试 // WAL 测试
// =================================================================== // ===================================================================
describe('AriaEngine — WAL', () => { describe('AriaEngine — WAL', () => {
it('append 记录后可恢复', async () => { it('append 记录后可恢复', async () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, true, 'full'); 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: '1', data: { name: 'Alice' } });
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '2', data: { name: 'Bob' } }); wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 'users', key: '2', data: { name: 'Bob' } });
const records: WALRecord[] = []; const records: WALRecord[] = [];
await wal.recover((r) => records.push(r)); await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2); expect(records).toHaveLength(2);
expect(records[0].tableName).toBe('users'); expect(records[0].tableName).toBe('users');
expect(records[0].key).toBe('1'); expect(records[0].key).toBe('1');
}); });
it('batch 模式缓冲后 flush', async () => { it('batch 模式缓冲后 flush', async () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, true, 'batch'); 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.UPDATE, txnId: 2, tableName: 'items', key: 'a', data: { v: 1 } });
wal.append({ type: WALRecordType.DELETE, txnId: 2, tableName: 'items', key: 'b' }); wal.append({ type: WALRecordType.DELETE, txnId: 2, tableName: 'items', key: 'b' });
// 未 flush 前无法恢复 // 未 flush 前无法恢复
let records: WALRecord[] = []; let records: WALRecord[] = [];
await wal.recover((r) => records.push(r)); await wal.recover((r) => records.push(r));
expect(records).toHaveLength(0); expect(records).toHaveLength(0);
await wal.flush(); await wal.flush();
records = []; records = [];
await wal.recover((r) => records.push(r)); await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2); expect(records).toHaveLength(2);
}); });
it('none 模式不记录', async () => { it('none 模式不记录', async () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, false, 'none'); const wal = new WAL(store, false, 'none');
wal.append({ type: WALRecordType.INSERT, txnId: 3, tableName: 'x', key: 'y', data: {} }); wal.append({ type: WALRecordType.INSERT, txnId: 3, tableName: 'x', key: 'y', data: {} });
expect(store.chunks).toHaveLength(0); expect(store.chunks).toHaveLength(0);
}); });
it('checkpoint 清空 WAL', async () => { it('checkpoint 清空 WAL', async () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, true, 'full'); const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.CREATE_TABLE, txnId: 0, tableName: 't', key: '' }); wal.append({ type: WALRecordType.CREATE_TABLE, txnId: 0, tableName: 't', key: '' });
expect(await store.exists()).toBe(true); expect(await store.exists()).toBe(true);
await wal.checkpoint(); await wal.checkpoint();
expect(await store.exists()).toBe(false); expect(await store.exists()).toBe(false);
}); });
it('getLSN 跟踪序列号', () => { it('getLSN 跟踪序列号', () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, true, 'full'); const wal = new WAL(store, true, 'full');
expect(wal.getLSN()).toBe(0); expect(wal.getLSN()).toBe(0);
wal.append({ type: WALRecordType.BEGIN, txnId: 10, tableName: '', key: '' }); wal.append({ type: WALRecordType.BEGIN, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(1); expect(wal.getLSN()).toBe(1);
wal.append({ type: WALRecordType.COMMIT, txnId: 10, tableName: '', key: '' }); wal.append({ type: WALRecordType.COMMIT, txnId: 10, tableName: '', key: '' });
expect(wal.getLSN()).toBe(2); expect(wal.getLSN()).toBe(2);
}); });
it('isEnabled 反映配置', () => { it('isEnabled 反映配置', () => {
expect(new WAL(new MockWALStore(), true).isEnabled()).toBe(true); expect(new WAL(new MockWALStore(), true).isEnabled()).toBe(true);
expect(new WAL(new MockWALStore(), false).isEnabled()).toBe(false); expect(new WAL(new MockWALStore(), false).isEnabled()).toBe(false);
}); });
it('多种记录类型编解码', async () => { it('多种记录类型编解码', async () => {
const store = new MockWALStore(); const store = new MockWALStore();
const wal = new WAL(store, true, 'full'); const wal = new WAL(store, true, 'full');
wal.append({ type: WALRecordType.BEGIN, txnId: 100, tableName: '', key: '' }); 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.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.UPDATE, txnId: 100, tableName: 'users', key: '1', data: { x: 'world' } });
wal.append({ type: WALRecordType.COMMIT, txnId: 100, tableName: '', key: '' }); wal.append({ type: WALRecordType.COMMIT, txnId: 100, tableName: '', key: '' });
const records: WALRecord[] = []; const records: WALRecord[] = [];
await wal.recover((r) => records.push(r)); await wal.recover((r) => records.push(r));
expect(records).toHaveLength(4); expect(records).toHaveLength(4);
expect(records[0].type).toBe(WALRecordType.BEGIN); expect(records[0].type).toBe(WALRecordType.BEGIN);
expect(records[1].type).toBe(WALRecordType.INSERT); expect(records[1].type).toBe(WALRecordType.INSERT);
expect(records[2].type).toBe(WALRecordType.UPDATE); expect(records[2].type).toBe(WALRecordType.UPDATE);
expect(records[3].type).toBe(WALRecordType.COMMIT); expect(records[3].type).toBe(WALRecordType.COMMIT);
}); });
it('getBufferedCount 返回缓冲数', () => { it('getBufferedCount 返回缓冲数', () => {
const wal = new WAL(new MockWALStore(), true, 'batch'); const wal = new WAL(new MockWALStore(), true, 'batch');
expect(wal.getBufferedCount()).toBe(0); expect(wal.getBufferedCount()).toBe(0);
wal.append({ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: 'k' }); wal.append({ type: WALRecordType.INSERT, txnId: 0, tableName: 't', key: 'k' });
expect(wal.getBufferedCount()).toBe(1); expect(wal.getBufferedCount()).toBe(1);
}); });
}); });
// =================================================================== // ===================================================================
// Checkpoint 测试(使用安全 Mock,避免 null 引用导致 CI 卡死) // Checkpoint 测试(使用安全 Mock,避免 null 引用导致 CI 卡死)
// =================================================================== // ===================================================================
describe('AriaEngine — CheckpointManager', () => { describe('AriaEngine — CheckpointManager', () => {
class MockFlushable implements Flushable { flushed = false; async flushAll() { this.flushed = true; } } class MockFlushable implements Flushable { flushed = false; async flushAll() { this.flushed = true; } }
class MockLSM { flushed = false; async flush() { this.flushed = true; } } class MockLSM { flushed = false; async flush() { this.flushed = true; } }
class MockWAL { checkpointed = false; async checkpoint() { this.checkpointed = true; } async flush() {} } class MockWAL { checkpointed = false; async checkpoint() { this.checkpointed = true; } async flush() {} }
it('tick 未达间隔不触发 checkpoint', async () => { it('tick 未达间隔不触发 checkpoint', async () => {
const lsm = new MockLSM(); const lsm = new MockLSM();
const wal = new MockWAL(); const wal = new MockWAL();
const flushable = new MockFlushable(); const flushable = new MockFlushable();
const cm = new CheckpointManager(lsm as any, wal as any, flushable, 100); const cm = new CheckpointManager(lsm as any, wal as any, flushable, 100);
await cm.tick(); await cm.tick();
await cm.tick(); await cm.tick();
expect(cm.getOpCount()).toBe(2); expect(cm.getOpCount()).toBe(2);
expect(flushable.flushed).toBe(false); expect(flushable.flushed).toBe(false);
expect(lsm.flushed).toBe(false); expect(lsm.flushed).toBe(false);
}); });
it('setInterval 修改间隔后 tick 触发 checkpoint', async () => { it('setInterval 修改间隔后 tick 触发 checkpoint', async () => {
const lsm = new MockLSM(); const lsm = new MockLSM();
const wal = new MockWAL(); const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 1000); const cm = new CheckpointManager(lsm as any, wal as any, null, 1000);
cm.setInterval(2); cm.setInterval(2);
await cm.tick(); await cm.tick();
await cm.tick(); await cm.tick();
expect(cm.getOpCount()).toBe(0); // reset after checkpoint expect(cm.getOpCount()).toBe(0); // reset after checkpoint
expect(lsm.flushed).toBe(true); expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true); expect(wal.checkpointed).toBe(true);
}); });
it('forceCheckpoint 强制触发', async () => { it('forceCheckpoint 强制触发', async () => {
const lsm = new MockLSM(); const lsm = new MockLSM();
const wal = new MockWAL(); const wal = new MockWAL();
const cm = new CheckpointManager(lsm as any, wal as any, null, 100); const cm = new CheckpointManager(lsm as any, wal as any, null, 100);
await cm.forceCheckpoint(); await cm.forceCheckpoint();
expect(cm.getOpCount()).toBe(0); expect(cm.getOpCount()).toBe(0);
expect(lsm.flushed).toBe(true); expect(lsm.flushed).toBe(true);
expect(wal.checkpointed).toBe(true); expect(wal.checkpointed).toBe(true);
}); });
}); });
// =================================================================== // ===================================================================
// MVCC 测试 // MVCC 测试
// =================================================================== // ===================================================================
describe('AriaEngine — MVCC', () => { describe('AriaEngine — MVCC', () => {
let mvcc: MVCCManager; let mvcc: MVCCManager;
beforeEach(() => { mvcc = new MVCCManager(); }); beforeEach(() => { mvcc = new MVCCManager(); });
it('beginTransaction 分配唯一 ID', () => { it('beginTransaction 分配唯一 ID', () => {
const id1 = mvcc.beginTransaction(); const id1 = mvcc.beginTransaction();
const id2 = mvcc.beginTransaction(); const id2 = mvcc.beginTransaction();
expect(id1).not.toBe(id2); expect(id1).not.toBe(id2);
expect(mvcc.isActive(id1)).toBe(true); expect(mvcc.isActive(id1)).toBe(true);
expect(mvcc.isActive(id2)).toBe(true); expect(mvcc.isActive(id2)).toBe(true);
}); });
it('commit 后 isActive 返回 false', () => { it('commit 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.commitTransaction(txnId); mvcc.commitTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false); expect(mvcc.isActive(txnId)).toBe(false);
}); });
it('rollback 后 isActive 返回 false', () => { it('rollback 后 isActive 返回 false', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.rollbackTransaction(txnId); mvcc.rollbackTransaction(txnId);
expect(mvcc.isActive(txnId)).toBe(false); expect(mvcc.isActive(txnId)).toBe(false);
}); });
it('writeVersion + readVersion 往返', () => { it('writeVersion + readVersion 往返', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice', age: 30 }, txnId); mvcc.writeVersion('users', '1', { name: 'Alice', age: 30 }, txnId);
const val = mvcc.readVersion('users', '1', txnId); const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull(); expect(val).not.toBeNull();
expect(val!.name).toBe('Alice'); expect(val!.name).toBe('Alice');
}); });
it('未提交版本对其他事务不可见', () => { it('未提交版本对其他事务不可见', () => {
const txn1 = mvcc.beginTransaction(); const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1); mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
const txn2 = mvcc.beginTransaction(); const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2); const val = mvcc.readVersion('users', '1', txn2);
expect(val).toBeNull(); // txn1's write not yet committed expect(val).toBeNull(); // txn1's write not yet committed
}); });
it('commit 后新事务可见', () => { it('commit 后新事务可见', () => {
const txn1 = mvcc.beginTransaction(); const txn1 = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1); mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
mvcc.commitTransaction(txn1); mvcc.commitTransaction(txn1);
const txn2 = mvcc.beginTransaction(); const txn2 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn2); const val = mvcc.readVersion('users', '1', txn2);
expect(val).not.toBeNull(); expect(val).not.toBeNull();
expect(val!.name).toBe('Alice'); expect(val!.name).toBe('Alice');
}); });
it('rollback 移除写入的版本', () => { it('rollback 移除写入的版本', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Temp' }, txnId); mvcc.writeVersion('users', '1', { name: 'Temp' }, txnId);
mvcc.rollbackTransaction(txnId); mvcc.rollbackTransaction(txnId);
const txn2 = mvcc.beginTransaction(); const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull(); expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
}); });
it('deleteVersion 创建墓碑', () => { it('deleteVersion 创建墓碑', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId); mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.commitTransaction(txnId); mvcc.commitTransaction(txnId);
// delete // delete
const txn2 = mvcc.beginTransaction(); const txn2 = mvcc.beginTransaction();
mvcc.deleteVersion('users', '1', txn2); mvcc.deleteVersion('users', '1', txn2);
mvcc.commitTransaction(txn2); mvcc.commitTransaction(txn2);
// 删除后读取 // 删除后读取
const txn3 = mvcc.beginTransaction(); const txn3 = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txn3); const val = mvcc.readVersion('users', '1', txn3);
expect(val).not.toBeNull(); expect(val).not.toBeNull();
expect((val! as any).__mvcc_tombstone).toBe(true); expect((val! as any).__mvcc_tombstone).toBe(true);
}); });
it('getLatestCommittedVersions 返回最新已提交', () => { it('getLatestCommittedVersions 返回最新已提交', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId); mvcc.writeVersion('users', '1', { name: 'Alice' }, txnId);
mvcc.writeVersion('users', '2', { name: 'Bob' }, txnId); mvcc.writeVersion('users', '2', { name: 'Bob' }, txnId);
mvcc.commitTransaction(txnId); mvcc.commitTransaction(txnId);
const result = mvcc.getLatestCommittedVersions('users'); const result = mvcc.getLatestCommittedVersions('users');
expect(result['1'].name).toBe('Alice'); expect(result['1'].name).toBe('Alice');
expect(result['2'].name).toBe('Bob'); expect(result['2'].name).toBe('Bob');
}); });
it('clearTable 清理指定表', () => { it('clearTable 清理指定表', () => {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { name: 'A' }, txnId); mvcc.writeVersion('users', '1', { name: 'A' }, txnId);
mvcc.writeVersion('users', '2', { name: 'B' }, txnId); mvcc.writeVersion('users', '2', { name: 'B' }, txnId);
mvcc.writeVersion('orders', 'o1', { amt: 100 }, txnId); mvcc.writeVersion('orders', 'o1', { amt: 100 }, txnId);
mvcc.commitTransaction(txnId); mvcc.commitTransaction(txnId);
mvcc.clearTable('users'); mvcc.clearTable('users');
const txn2 = mvcc.beginTransaction(); const txn2 = mvcc.beginTransaction();
expect(mvcc.readVersion('users', '1', txn2)).toBeNull(); expect(mvcc.readVersion('users', '1', txn2)).toBeNull();
expect(mvcc.readVersion('users', '2', txn2)).toBeNull(); expect(mvcc.readVersion('users', '2', txn2)).toBeNull();
expect(mvcc.readVersion('orders', 'o1', txn2)).not.toBeNull(); expect(mvcc.readVersion('orders', 'o1', txn2)).not.toBeNull();
}); });
it('getActiveTxnCount 返回活跃事务数', () => { it('getActiveTxnCount 返回活跃事务数', () => {
expect(mvcc.getActiveTxnCount()).toBe(0); expect(mvcc.getActiveTxnCount()).toBe(0);
mvcc.beginTransaction(); mvcc.beginTransaction();
mvcc.beginTransaction(); mvcc.beginTransaction();
expect(mvcc.getActiveTxnCount()).toBe(2); expect(mvcc.getActiveTxnCount()).toBe(2);
}); });
it('gc 清理过旧版本', () => { it('gc 清理过旧版本', () => {
// 创建很多版本后 gc // 创建很多版本后 gc
for (let i = 0; i < 200; i++) { for (let i = 0; i < 200; i++) {
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
mvcc.writeVersion('users', '1', { ver: i }, txnId); mvcc.writeVersion('users', '1', { ver: i }, txnId);
mvcc.commitTransaction(txnId); mvcc.commitTransaction(txnId);
} }
mvcc.gc(100); mvcc.gc(100);
// gc 后应可继续操作 // gc 后应可继续操作
const txnId = mvcc.beginTransaction(); const txnId = mvcc.beginTransaction();
const val = mvcc.readVersion('users', '1', txnId); const val = mvcc.readVersion('users', '1', txnId);
expect(val).not.toBeNull(); 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 * utils.ts
*/ */
import { import {
generateId, generateId,
escapeHTML, escapeHTML,
debounce, debounce,
throttle, throttle,
deepMerge, deepMerge,
isBrowser, isBrowser,
} from '../src/utils'; } from '../src/utils';
describe('generateId', () => { describe('generateId', () => {
test('返回字符串', () => { test('返回字符串', () => {
expect(typeof generateId()).toBe('string'); expect(typeof generateId()).toBe('string');
}); });
test('多次调用产生不同值', () => { test('多次调用产生不同值', () => {
const ids = new Set<string>(); const ids = new Set<string>();
for (let i = 0; i < 100; i++) ids.add(generateId()); for (let i = 0; i < 100; i++) ids.add(generateId());
expect(ids.size).toBe(100); expect(ids.size).toBe(100);
}); });
}); });
describe('escapeHTML', () => { describe('escapeHTML', () => {
test('转义 < > &', () => { test('转义 < > &', () => {
const out = escapeHTML('<a>&b</a>'); const out = escapeHTML('<a>&b</a>');
expect(out).toContain('&lt;'); expect(out).toContain('&lt;');
expect(out).toContain('&gt;'); expect(out).toContain('&gt;');
expect(out).toContain('&amp;'); expect(out).toContain('&amp;');
}); });
test('null/undefined 返回空字符串', () => { test('null/undefined 返回空字符串', () => {
expect(escapeHTML(null)).toBe(''); expect(escapeHTML(null)).toBe('');
expect(escapeHTML(undefined)).toBe(''); expect(escapeHTML(undefined)).toBe('');
}); });
}); });
describe('debounce', () => { describe('debounce', () => {
test('延迟执行', (done) => { test('延迟执行', (done) => {
let called = 0; let called = 0;
const fn = debounce(() => { called++; }, 20); const fn = debounce(() => { called++; }, 20);
fn(); fn();
expect(called).toBe(0); expect(called).toBe(0);
setTimeout(() => { setTimeout(() => {
expect(called).toBe(1); expect(called).toBe(1);
done(); done();
}, 50); }, 50);
}); });
test('多次调用只执行最后一次', (done) => { test('多次调用只执行最后一次', (done) => {
let result = 0; let result = 0;
const fn = debounce((v: number) => { result = v; }, 20); const fn = debounce((v: number) => { result = v; }, 20);
fn(1); fn(1);
fn(2); fn(2);
fn(3); fn(3);
setTimeout(() => { setTimeout(() => {
expect(result).toBe(3); expect(result).toBe(3);
done(); done();
}, 50); }, 50);
}); });
}); });
describe('throttle', () => { describe('throttle', () => {
test('首次立即执行', () => { test('首次立即执行', () => {
let called = 0; let called = 0;
const fn = throttle(() => { called++; }, 50); const fn = throttle(() => { called++; }, 50);
fn(); fn();
expect(called).toBe(1); expect(called).toBe(1);
}); });
test('限流期内不重复执行', () => { test('限流期内不重复执行', () => {
let called = 0; let called = 0;
const fn = throttle(() => { called++; }, 50); const fn = throttle(() => { called++; }, 50);
fn(); fn();
fn(); fn();
fn(); fn();
expect(called).toBe(1); expect(called).toBe(1);
}); });
}); });
describe('deepMerge', () => { describe('deepMerge', () => {
test('浅层合并', () => { test('浅层合并', () => {
const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 }); const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 });
expect(out).toEqual({ a: 1, b: 3, c: 4 }); expect(out).toEqual({ a: 1, b: 3, c: 4 });
}); });
test('深层对象合并', () => { test('深层对象合并', () => {
const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } }); const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } });
expect(out).toEqual({ obj: { x: 1, y: 2 } }); expect(out).toEqual({ obj: { x: 1, y: 2 } });
}); });
}); });
describe('isBrowser', () => { describe('isBrowser', () => {
test('jsdom 环境下返回 true', () => { test('jsdom 环境下返回 true', () => {
expect(isBrowser()).toBe(true); expect(isBrowser()).toBe(true);
}); });
}); });
+361 -374
View File
@@ -1,374 +1,361 @@
/** /**
* v0.2.5 * v0.2.5
* P0/P1/P2 * P0/P1/P2
*/ */
import { VERSION } from '../src/constants'; import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core'; import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index'; import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory'; import { MemoryEngine } from '../src/engine/memory';
import { parse } from '../src/sql/parser'; import { parse } from '../src/sql/parser';
import { QueryExecutor } from '../src/query/executor'; import { QueryExecutor } from '../src/query/executor';
import { WAL } from '../src/engine/aria/wal/log'; import { WAL } from '../src/engine/aria/wal/log';
import { SSTableReader } from '../src/engine/aria/index/sstable'; import { SSTableReader } from '../src/engine/aria/index/sstable';
import { BloomFilter } from '../src/engine/aria/index/bloom'; import { BloomFilter } from '../src/engine/aria/index/bloom';
import { CryptoManager } from '../src/engine/aria/crypto'; import { CryptoManager } from '../src/engine/aria/crypto';
import { PluginManager } from '../src/plugin/index'; import { PluginManager } from '../src/plugin/index';
import type { SSTableMeta } from '../src/engine/aria/types'; import type { SSTableMeta } from '../src/engine/aria/types';
import type { MetonaPlugin } from '../src/constants'; import type { MetonaPlugin } from '../src/constants';
import { createSchema } from '../src/table/schema'; import { createSchema } from '../src/table/schema';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-1: 版本号统一 // P0-1: 版本号统一
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => { describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为 0.2.5', () => { test('VERSION 常量为当前版本(0.3.2', () => {
expect(VERSION).toBe('0.2.5'); expect(VERSION).toBe('0.3.2');
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-2: AriaEngine OPFS 后端映射修复 // P0-2: AriaEngine OPFS 后端映射修复
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => { describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => {
test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => { test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => {
const db = new MetonaSqlark({ name: 'test-opfs-map', mode: 'aria', diskEngine: 'opfs' }); const db = new MetonaSqlark({ name: 'test-opfs-map', mode: 'aria', diskEngine: 'opfs' });
// 不实际 open(需要浏览器环境),只验证 createEngine 逻辑 // 不实际 open(需要浏览器环境),只验证 createEngine 逻辑
// 通过 getEngine 在 init 后检查 // 通过 getEngine 在 init 后检查
expect(db).toBeDefined(); expect(db).toBeDefined();
}); });
test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => { test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => {
const db = new MetonaSqlark({ name: 'test-idb-map', mode: 'aria', diskEngine: 'indexeddb' }); const db = new MetonaSqlark({ name: 'test-idb-map', mode: 'aria', diskEngine: 'indexeddb' });
expect(db).toBeDefined(); expect(db).toBeDefined();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-3: _onError 接入执行路径 // P0-3: _onError 接入执行路径
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-3: _onError 接入执行路径', () => { describe('[v0.2.5] P0-3: _onError 接入执行路径', () => {
test('query 失败时调用 onError 回调', async () => { test('query 失败时调用 onError 回调', async () => {
const errors: Error[] = []; const errors: Error[] = [];
const db = new MetonaSqlark({ const db = new MetonaSqlark({
name: 'test-onerror', name: 'test-onerror',
mode: 'memory', mode: 'memory',
onError: (e) => errors.push(e), onError: (e) => errors.push(e),
}); });
await db.init(); await db.init();
await db.defineTable('users', { await db.defineTable('users', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
name: { type: 'string' }, name: { type: 'string' },
}); });
// 故意执行不存在的表查询 // 故意执行不存在的表查询
await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow(); await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0); expect(errors.length).toBeGreaterThan(0);
await db.close(); await db.close();
}); });
test('defineTable 失败时调用 onError', async () => { test('defineTable 失败时调用 onError', async () => {
const errors: Error[] = []; const errors: Error[] = [];
const db = new MetonaSqlark({ const db = new MetonaSqlark({
name: 'test-onerror2', name: 'test-onerror2',
mode: 'memory', mode: 'memory',
onError: (e) => errors.push(e), onError: (e) => errors.push(e),
}); });
await db.init(); await db.init();
await db.defineTable('dup', { await db.defineTable('dup', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
}); });
// 重复创建 // 重复创建
await expect(db.defineTable('dup', { await expect(db.defineTable('dup', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
})).rejects.toThrow(); })).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0); expect(errors.length).toBeGreaterThan(0);
await db.close(); await db.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-4: maxRowsPerQuery 生效 // P0-4: maxRowsPerQuery 生效
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => { describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => {
test('结果集被截断为 maxRowsPerQuery', async () => { test('结果集被截断为 maxRowsPerQuery', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-maxrows', 1); await engine.open('test-maxrows', 1);
await engine.createTable(createSchema('items', { await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
val: { type: 'number' }, val: { type: 'number' },
})); }));
// 插入 100 行 // 插入 100 行
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
await engine.insert('items', [{ id: `item${i}`, val: i }]); await engine.insert('items', [{ id: `item${i}`, val: i }]);
} }
const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10 const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10
const stmt = parse('SELECT * FROM items'); const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[]; const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(10); // 截断为 10 行 expect(result.length).toBe(10); // 截断为 10 行
await engine.close(); await engine.close();
}); });
test('maxRowsPerQuery=0 表示不限制', async () => { test('maxRowsPerQuery=0 表示不限制', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-nolimit', 1); await engine.open('test-nolimit', 1);
await engine.createTable(createSchema('items', { await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
})); }));
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
await engine.insert('items', [{ id: `i${i}` }]); await engine.insert('items', [{ id: `i${i}` }]);
} }
const executor = new QueryExecutor(engine, 0); const executor = new QueryExecutor(engine, 0);
const stmt = parse('SELECT * FROM items'); const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[]; const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(50); expect(result.length).toBe(50);
await engine.close(); await engine.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-5: WAL full 模式真正同步 // P0-5: WAL full 模式真正同步
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-5: WAL full 模式同步', () => { describe('[v0.2.5] P0-5: WAL full 模式同步', () => {
test('append 在 full 模式下是 async 且可 await', async () => { test('append 在 full 模式下是 async 且可 await', async () => {
let appendCount = 0; let appendCount = 0;
const wal = new WAL({ const wal = new WAL({
append: async (_data: Uint8Array) => { appendCount++; }, append: async (_data: Uint8Array) => { appendCount++; },
readAll: async () => new Uint8Array(0), readAll: async () => new Uint8Array(0),
truncate: async () => {}, truncate: async () => {},
exists: async () => false, exists: async () => false,
}, true, 'full'); }, true, 'full');
// append 现在返回 Promise // append 现在返回 Promise
await wal.append({ await wal.append({
type: 1, // INSERT type: 1, // INSERT
txnId: 0, txnId: 0,
tableName: 'test', tableName: 'test',
key: 'k1', key: 'k1',
data: { v: 1 }, data: { v: 1 },
} as any); } as any);
expect(appendCount).toBe(1); expect(appendCount).toBe(1);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-6: PluginManager.install 传 db 实例 // P0-6: PluginManager.install 传 db 实例
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => { describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => {
test('register 传 db 给 install', () => { test('register 传 db 给 install', () => {
let receivedDb: unknown = null; let receivedDb: unknown = null;
const plugin: MetonaPlugin = { const plugin: MetonaPlugin = {
name: 'test-plugin', name: 'test-plugin',
install: (db) => { receivedDb = db; }, install: (db) => { receivedDb = db; },
destroy: () => {}, destroy: () => {},
}; };
const pm = new PluginManager(); const pm = new PluginManager();
const fakeDb = { name: 'fake' }; const fakeDb = { name: 'fake' };
pm.register(plugin, fakeDb); pm.register(plugin, fakeDb);
expect(receivedDb).toBe(fakeDb); expect(receivedDb).toBe(fakeDb);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P1-7: SSTableReader 二分查找统一 // P1-7: SSTableReader 二分查找统一
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => { describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => {
test('rangeScan 使用二分查找正确定位', () => { test('rangeScan 使用二分查找正确定位', () => {
// 构建一个 SSTable 手工 // 构建一个 SSTable 手工
const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder'); const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder');
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
// 添加足够多的条目以形成多个 block // 添加足够多的条目以形成多个 block
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
const key = `key${String(i).padStart(5, '0')}`; const key = `key${String(i).padStart(5, '0')}`;
builder.add(key, { data: `value${i}` }); builder.add(key, { data: `value${i}` });
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const meta: SSTableMeta = { const meta: SSTableMeta = {
id: 1, id: 1,
level: 0, level: 0,
minKey: 'key00000', minKey: 'key00000',
maxKey: 'key00099', maxKey: 'key00099',
blockCount: 1, blockCount: 1,
totalSize: sstableData.byteLength, totalSize: sstableData.byteLength,
bloomData: null, bloomData: null,
}; };
const reader = new SSTableReader(sstableData, meta); const reader = new SSTableReader(sstableData, meta);
// 精确查找 // 精确查找
const result = reader.get('key00050'); const result = reader.get('key00050');
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect((result as any).data).toBe('value50'); expect((result as any).data).toBe('value50');
// 范围扫描 // 范围扫描
const collected: string[] = []; const collected: string[] = [];
reader.rangeScan('key00010', 'key00020', (k) => collected.push(k)); reader.rangeScan('key00010', 'key00020', (k) => collected.push(k));
expect(collected.length).toBeGreaterThan(0); expect(collected.length).toBeGreaterThan(0);
expect(collected[0]).toBe('key00010'); expect(collected[0]).toBe('key00010');
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P1-8: SQL 注入防护 — 表名校验 // P1-8: SQL 注入防护 — 表名校验
// --------------------------------------------------------------------------- // 真实覆盖位置:tests/integrations/react.test.tsuseTable
// tests/integrations/vue.test.tsuseSqlarkTable
describe('[v0.2.5] P1-8: SQL 注入防护', () => { // 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。
test('表名校验正则表达式正确', () => { // 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。
// 验证正则逻辑本身 // ---------------------------------------------------------------------------
const validName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
expect(validName.test('users')).toBe(true); // ---------------------------------------------------------------------------
expect(validName.test('user_table')).toBe(true); // P1-9: crypto 实例化
expect(validName.test('_private')).toBe(true); // ---------------------------------------------------------------------------
expect(validName.test('Table1')).toBe(true);
// 非法表名 describe('[v0.2.5] P1-9: CryptoManager 实例化', () => {
expect(validName.test('users; DROP TABLE')).toBe(false); test('CryptoManager 可以独立实例化', () => {
expect(validName.test('1table')).toBe(false); const cm1 = new CryptoManager();
expect(validName.test('user.name')).toBe(false); const cm2 = new CryptoManager();
expect(validName.test('user name')).toBe(false); expect(cm1.enabled).toBe(false);
expect(validName.test("'; DROP TABLE users; --")).toBe(false); expect(cm2.enabled).toBe(false);
}); // 两个实例互不影响
}); expect(cm1).not.toBe(cm2);
});
// --------------------------------------------------------------------------- });
// P1-9: crypto 实例化
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-14: ALTER TABLE 语法
describe('[v0.2.5] P1-9: CryptoManager 实例化', () => { // ---------------------------------------------------------------------------
test('CryptoManager 可以独立实例化', () => {
const cm1 = new CryptoManager(); describe('[v0.2.5] P2-14: ALTER TABLE', () => {
const cm2 = new CryptoManager(); test('解析 ALTER TABLE ADD COLUMN', () => {
expect(cm1.enabled).toBe(false); const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(cm2.enabled).toBe(false); expect(stmt.type).toBe('ALTER_TABLE');
// 两个实例互不影响 expect((stmt as any).name).toBe('users');
expect(cm1).not.toBe(cm2); expect((stmt as any).action).toBe('ADD');
}); expect((stmt as any).column.name).toBe('email');
}); });
// --------------------------------------------------------------------------- test('解析 ALTER TABLE DROP COLUMN', () => {
// P2-14: ALTER TABLE 语法 const stmt = parse('ALTER TABLE users DROP COLUMN email');
// --------------------------------------------------------------------------- expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).action).toBe('DROP');
describe('[v0.2.5] P2-14: ALTER TABLE', () => { expect((stmt as any).column.name).toBe('email');
test('解析 ALTER TABLE ADD COLUMN', () => { });
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(stmt.type).toBe('ALTER_TABLE'); test('执行 ALTER TABLE ADD COLUMN', async () => {
expect((stmt as any).name).toBe('users'); const engine = new MemoryEngine();
expect((stmt as any).action).toBe('ADD'); await engine.open('test-alter', 1);
expect((stmt as any).column.name).toBe('email'); await engine.createTable(createSchema('users', {
}); id: { type: 'string', primaryKey: true },
name: { type: 'string' },
test('解析 ALTER TABLE DROP COLUMN', () => { }));
const stmt = parse('ALTER TABLE users DROP COLUMN email');
expect(stmt.type).toBe('ALTER_TABLE'); const executor = new QueryExecutor(engine);
expect((stmt as any).action).toBe('DROP'); const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
expect((stmt as any).column.name).toBe('email'); await executor.execute(stmt);
});
const schema = await engine.getTableSchema('users');
test('执行 ALTER TABLE ADD COLUMN', async () => { expect(schema!.columns.email).toBeDefined();
const engine = new MemoryEngine(); await engine.close();
await engine.open('test-alter', 1); });
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, test('执行 ALTER TABLE DROP COLUMN', async () => {
name: { type: 'string' }, const engine = new MemoryEngine();
})); await engine.open('test-alter-drop', 1);
await engine.createTable(createSchema('users', {
const executor = new QueryExecutor(engine); id: { type: 'string', primaryKey: true },
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)'); name: { type: 'string' },
await executor.execute(stmt); email: { type: 'string' },
}));
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeDefined(); const executor = new QueryExecutor(engine);
await engine.close(); const stmt = parse('ALTER TABLE users DROP COLUMN email');
}); await executor.execute(stmt);
test('执行 ALTER TABLE DROP COLUMN', async () => { const schema = await engine.getTableSchema('users');
const engine = new MemoryEngine(); expect(schema!.columns.email).toBeUndefined();
await engine.open('test-alter-drop', 1); await engine.close();
await engine.createTable(createSchema('users', { });
id: { type: 'string', primaryKey: true }, });
name: { type: 'string' },
email: { type: 'string' }, // ---------------------------------------------------------------------------
})); // P2-15: TRUNCATE TABLE 语法
// ---------------------------------------------------------------------------
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users DROP COLUMN email'); describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => {
await executor.execute(stmt); test('解析 TRUNCATE TABLE', () => {
const stmt = parse('TRUNCATE TABLE users');
const schema = await engine.getTableSchema('users'); expect(stmt.type).toBe('TRUNCATE_TABLE');
expect(schema!.columns.email).toBeUndefined(); expect((stmt as any).name).toBe('users');
await engine.close(); });
});
}); test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine();
// --------------------------------------------------------------------------- await engine.open('test-truncate', 1);
// P2-15: TRUNCATE TABLE 语法 await engine.createTable(createSchema('items', {
// --------------------------------------------------------------------------- id: { type: 'string', primaryKey: true },
}));
describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => { await engine.insert('items', [
test('解析 TRUNCATE TABLE', () => { { id: 'a' }, { id: 'b' }, { id: 'c' },
const stmt = parse('TRUNCATE TABLE users'); ]);
expect(stmt.type).toBe('TRUNCATE_TABLE');
expect((stmt as any).name).toBe('users'); const executor = new QueryExecutor(engine);
}); const stmt = parse('TRUNCATE TABLE items');
await executor.execute(stmt);
test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine(); const rows = await engine.find('items', { table: 'items' });
await engine.open('test-truncate', 1); expect(rows.length).toBe(0);
await engine.createTable(createSchema('items', { await engine.close();
id: { type: 'string', primaryKey: true }, });
})); });
await engine.insert('items', [
{ id: 'a' }, { id: 'b' }, { id: 'c' }, // ---------------------------------------------------------------------------
]); // P2-12: WAL 大小阈值接入 checkpoint
// ---------------------------------------------------------------------------
const executor = new QueryExecutor(engine);
const stmt = parse('TRUNCATE TABLE items'); describe('[v0.2.5] P2-12: WAL 大小阈值', () => {
await executor.execute(stmt); test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint');
const rows = await engine.find('items', { table: 'items' }); const fakeLsm = { flush: async () => {} };
expect(rows.length).toBe(0); const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 };
await engine.close(); const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024);
}); expect(cm).toBeDefined();
}); expect(cm.getOpCount()).toBe(0);
});
// --------------------------------------------------------------------------- });
// P2-12: WAL 大小阈值接入 checkpoint
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-13: compactLevelSync 接口公开化
describe('[v0.2.5] P2-12: WAL 大小阈值', () => { // ---------------------------------------------------------------------------
test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint'); describe('[v0.2.5] P2-13: compactLevel public', () => {
const fakeLsm = { flush: async () => {} }; test('LSM.compactLevel 是 public 方法', () => {
const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 }; const { LSM } = require('../src/engine/aria/index/lsm');
const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024); const lsm = new LSM({
expect(cm).toBeDefined(); sstableStore: {
expect(cm.getOpCount()).toBe(0); save: async () => {}, load: async () => null, delete: async () => {},
}); allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {},
}); },
});
// --------------------------------------------------------------------------- expect(typeof lsm.compactLevel).toBe('function');
// 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": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"module": "ES2020", "module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "node", "moduleResolution": "node",
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": false, "declaration": false,
"declarationMap": false, "declarationMap": false,
"sourceMap": true, "sourceMap": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"baseUrl": ".", "baseUrl": ".",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"] "exclude": ["node_modules", "dist", "tests"]
} }