thzxx 83c5aa0b9d fix(A11): AND 优先级高于 OR(SQL 标准)—— parser 条件表达式三层分层
此前 parseCondition 对 AND/OR 做纯左折叠:
  a = 1 OR a = 2 AND b = 3  →  (a = 1 OR a = 2) AND b = 3   (错,返回 1 行)
标准语义:a = 1 OR (a = 2 AND b = 3)                         (对,返回 3 行)

任何"权限条件 OR 业务条件 AND 软删标记"的写法都会静默返回错误行集。

改为标准文法分层:parseCondition → parseOrExpression → parseAndExpression
→ parseSimpleCondition(NOT 已在内层处理,结合性正确)。
仅在确有多个操作数时才包 $and/$or,避免产生 {$and:[x]} 冗余节点而破坏
既有 AST 契约与下游索引下推识别。

实测:WHERE a = 1 OR a = 2 AND b = 3 现返回 [1,2,4](此前 [2,4]),
与显式括号写法结果一致。parser/SQL 全部 111 用例通过。
2026-09-14 21:03:58 +08:00

MetonaSqlark

version license coverage tests

基于 TypeScript 的前端关系型数据库:完整 SQL + Query Builder 双 API 5 种存储引擎可选,内置自研 LSM-Tree 存储引擎(AriaEngine)。 零运行时依赖,浏览器 / Node.js 开箱即用。


目录


核心特性

数据库能力

  • 完整 SQL — SELECTJOIN / 子查询 / 派生表 / UNION / GROUP BY / HAVING / DISTINCT / CASE WHEN / EXISTS / BETWEEN / NULLS 排序)、INSERT...SELECT、UPDATE/DELETE 子查询、ALTER TABLE、TRUNCATE TABLE、CREATE INDEX、事务语句(BEGIN / COMMIT / ROLLBACK / SAVEPOINT)、维护语句(EXPLAIN / ANALYZE / REINDEX / VACUUM
  • Query Builder — 链式 .select().where().innerJoin().orderBy().limit().execute(),类型安全
  • 流式查询queryStream / table().stream() 逐行回调,Aria 引擎真惰性扫描(limit 提前终止,不物化结果集)
  • 事务 — 四引擎事务原子性 + 自动回滚;Aria 用 MVCC 快照隔离,事务读写不互斥
  • 外键级联ON DELETE / ON UPDATE 支持 CASCADE / SET NULL / RESTRICT(含主键变更级联、级联环路保护)
  • 数据迁移 — 版本化迁移(持久化到库内,重启不重跑)、导入导出、在线一致性备份
  • 连接池MetonaSqlark.connect() 单例复用,引用计数自动关闭

存储引擎(5 种)

  • memory — 纯内存,测试 / 缓存
  • disk — 自研 KVStore 事务引擎v0.6.0 起替代 IndexedDB):多 key 原子写、快照 + 日志崩溃恢复
  • hybrid — write-through 双写,读走内存
  • aria — 自研 AriaEngineLSM-Tree + WAL + MVCC),后端可选 OPFS / KVStore / Memory
  • KVStore — 日志结构化事务 KV 引擎,可独立用作 aria 后端

生产级可靠性

  • 崩溃恢复 — WAL 原子写入 + CRC-32 完整性校验(SSTable 整文件 + WAL 记录)、空洞检测截断、打开时损坏自愈、repair() 清理重建
  • 全库加密 — AES-256-GCM 透明加密(PBKDF2 密钥派生 + 密码验证 + 篡改检测),encryption.password 一键启用
  • 多标签页独占锁 — Web Locks API,第二个标签页打开同一库抛 ARIA_LOCKED
  • 数据安全 — 输入校验(required / maxLength / min / max)、SQL 注入防护、Bloom Filter 快速否定
  • 大表性能 — 10 万行级验证;批量插入组提交;SSTable 4KB 页面化 + BufferPool LRU 缓存

生态

  • React / Vue 集成useQuery / useSqlarkQuery 等开箱即用 hooks
  • 插件系统 — 14 种生命周期钩子(beforeInsert / afterQuery / ...),按优先级注册
  • 旧库迁移migrateFromIndexedDB() 一键把旧 IndexedDB 库导入新引擎
  • 浏览器兼容 — Chrome 102+ / Firefox 111+ / Safari 15.2+ / Edge 102+ / Node.js 16+

安装

npm install @metona-team/metona-sqlark

私有 registry(一次性配置):

npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/

CDN / 直接下载

<!-- UMD 格式,暴露 window.MetonaSqlark 与 window.MeSqlark -->
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>

或从 dist/ 下载:metona-sqlark.jsUMD 开发版)/ metona-sqlark.min.js(压缩版,gzip ~27KB/ metona-sqlark.esm.js / metona-sqlark.cjs / metona-sqlark.d.ts


快速开始

import { MetonaSqlark } from '@metona-team/metona-sqlark';
// 别名:import { MeSqlark } from '@metona-team/metona-sqlark';

const db = await MetonaSqlark.create({
  name: 'my-app',
  mode: 'hybrid',            // 'memory' | 'disk' | 'hybrid' | 'aria'
  diskEngine: 'opfs',        // 'opfs' | 'kv'(自研 KVStore 后端)| 'memory'
});

// 定义表 — 支持约束与外键级联
await db.defineTable('users', {
  id: { type: 'string', primaryKey: true },
  name: { type: 'string', required: true },
  age: { type: 'number', default: 0 },
  email: { type: 'string', unique: true, index: true },
});
await db.defineTable('orders', {
  id: { type: 'string', primaryKey: true },
  user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
  amount: { type: 'number' },
});

// SQL 查询
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30, 'alice@demo.com')");
const rows = await db.query('SELECT * FROM users WHERE age > 18 ORDER BY name');

// 参数化查询(v0.7.0)— 位置参数 `?`,安全编码杜绝 SQL 注入
await db.query("INSERT INTO users VALUES (?, ?, ?, ?)", ['2', "O'Brien", 25, 'ob@demo.com']);
const row2 = await db.query('SELECT * FROM users WHERE name = ?', ["O'Brien"]);

// Query Builder
const result = await db.table('users')
  .select(['name', 'age'])
  .where({ age: { $gt: 18 } })
  .orderBy('age', 'desc')
  .limit(10)
  .execute();

// JOIN
await db.query(`SELECT u.name, o.amount FROM users u
  INNER JOIN orders o ON u.id = o.user_id WHERE o.amount > 100`);

// 子查询 / UNION / EXISTS
await db.query(`SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`);
await db.query(`SELECT name FROM users WHERE city = 'Beijing'
  UNION SELECT name FROM users WHERE age < 30`);
await db.query(`SELECT * FROM users u WHERE EXISTS
  (SELECT 1 FROM orders o WHERE o.user_id = u.id)`);

// GROUP BY / CASE WHEN
await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`);
await db.query(`SELECT name, CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`);

// ALTER TABLE / TRUNCATE TABLE
await db.query('ALTER TABLE users ADD COLUMN phone STRING');
await db.query('TRUNCATE TABLE old_logs');

// 事务 — 失败自动回滚
await db.transaction(async (trx) => {
  await trx.table('users').insert({ id: '3', name: 'Charlie' });
  await trx.table('orders').insert({ id: 'o1', user_id: '3', amount: 99 });
});

// 流式查询 — 大表逐行回调
await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => {
  processRow(row);
});

// 崩溃恢复自愈
await db.repair();
await db.clearAll();

// 连接池 — 同库复用
const db2 = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
// db2 === db
await db2.disconnect(); // 引用计数 -1

API 速览

数据库配置

属性 类型 默认 说明
name string 'metona-sqlark' 数据库名称
mode 'memory' | 'disk' | 'hybrid' | 'aria' 'hybrid' 存储模式
diskEngine 'opfs' | 'memory' | 'kv' 'opfs' 磁盘引擎(aria 模式下为存储后端;'kv' = 自研 KVStore
version number 1 版本号
maxRowsPerQuery number 0 查询结果行数上限(0 = 不限)
debug boolean false 调试模式
multiTabSync boolean false 多标签页同步(BroadcastChannel
plugins MetonaPlugin[] 插件列表
onReady (db) => void 就绪回调
onError (error) => void 全局错误回调
aria AriaEngineConfig AriaEngine 配置透传(walSyncMode / encryption / pageStorage / compression 等)

ColumnDef 列定义

属性 类型 说明
type 'string' | 'number' | 'boolean' | 'date' | 'json' 数据类型(必填)
primaryKey boolean 主键(建表时至少一个)
required boolean 必填
unique boolean 唯一约束
index boolean 创建二级索引
default unknown 默认值
references string 外键引用 'table.column'
onDelete 'CASCADE' | 'SET NULL' | 'RESTRICT' 删除级联
onUpdate 'CASCADE' | 'SET NULL' | 'RESTRICT' 更新级联(更新主键时触发)
maxLength number 字符串最大长度
min / max number 数字最小值 / 最大值

WHERE 操作符

操作符 含义 操作符 含义
$eq / 直接值 等于 $gt / $gte 大于 / 大于等于
$ne 不等于 $lt / $lte 小于 / 小于等于
$in / $nin 在列表中 $like 模糊匹配
$and / $or / $not 逻辑组合

核心方法

方法 说明
db.query(sql) / db.query(sql, params) 执行 SQL(支持分号多语句;v0.7.0 位置参数 ? 绑定)
db.queryStream(sql, onRow) 流式查询(逐行回调,不物化)
db.table(name) 获取表操作对象(insert / select / update / delete / count / stream / clear / drop
db.defineTable(name, cols) 定义表结构
db.dropTable(name) / db.getTableNames() 删除表 / 列出表
db.transaction(fn) 执行事务(自动回滚)
db.repair() 崩溃恢复自愈:校验清理损坏数据、重建索引
db.clearAll() 清空全部数据与表结构(保留库本身)
db.exportTable(name) / db.exportAll() 导出数据 JSON
db.importTable(name, data) 导入数据
db.backup() 在线备份:全库一致性快照
db.addMigration(v, fn) / db.migrateTo(v) 版本化数据迁移(版本持久化,重启不重跑)
db.subscribe(table, fn) 订阅表变更(返回退订函数)
db.on(hook, fn) 注册生命周期钩子(14 种)
db.close() 关闭数据库

维护语句(SQL 入口)

语句 说明 支持引擎
EXPLAIN SELECT ... 输出查询计划(type/table/where/usingIndex/estimatedRows/actualTimeMs 全部
ANALYZE [TABLE] name 收集表统计信息(行数/行大小/索引深度/列基数) Aria
REINDEX [TABLE] name 重建表二级索引 Aria
VACUUM 压缩 LSM + 清理 MVCC 碎片 Aria
SAVEPOINT name / ROLLBACK TO name / RELEASE name 嵌套事务保存点 Aria

不支持的引擎执行维护语句抛 NOT_SUPPORTED

旧库迁移(v0.6.0

IndexedDB 已从引擎中完全移除。旧版本(v0.5.x 及更早)的 disk 模式用户可通过一次性迁移工具导入:

import { migrateFromIndexedDB } from '@metona-team/metona-sqlark/migration';

const target = await MetonaSqlark.create({ name: 'my-app-new', mode: 'disk' });
const result = await migrateFromIndexedDB({
  dbName: 'my-app',
  engine: 'disk',            // 仅支持旧 disk 模式(IndexedDBEngine
  target,
  onProgress: (done, total, table) => console.log(`迁移 ${done}/${total}: ${table}`),
});
// result: { migratedTables, rowCount, skippedTables }

注:旧 aria 模式库为引擎私有格式(SSTable/WAL),无法按行迁移——请从应用层 exportAll() 后重新导入。

连接池

静态方法 说明
MetonaSqlark.connect(config) 获取或创建数据库实例(单例复用)
MetonaSqlark.disconnect(name) 释放连接(引用计数 -1
MetonaSqlark.disconnectAll() 强制关闭所有连接
MetonaSqlark.getActiveConnections() 获取活跃连接列表

存储引擎

特性 Memory Disk (KVStore) Hybrid Aria
持久化 重启丢失 KVStoreOPFS / 内存介质) 内存 + 磁盘 后端决定
事务 快照回滚 单日志记录原子写 双引擎(磁盘优先) MVCC 快照隔离
二级索引 Hash Hash(重启恢复) Hash LSM(重启恢复)
查询性能 O(1) PK O(1) PK(内存热路径) O(1) PK O(log n)
数据上限 内存 磁盘可用 磁盘可用 内存
全库加密 AES-GCM
多标签页锁 Web Locks
适用场景 缓存 / 测试 标准持久化(替代 IndexedDB 速度 + 持久化 大规模 / 分析
测试覆盖 30+ 50+(含 10 万级压测) 15+ 400+

选型建议

  • 临时数据 / 单元测试memory
  • 标准前端持久化替代 IndexedDB)→ diskKVStore,多 key 原子事务,10 万级验证)
  • 内存速度 + 磁盘持久化hybridwrite-through,读走内存)
  • 大规模 / 需要自研引擎可控性ariaLSM-Tree + WAL + MVCC + 加密 + 页面化存储)

AriaEngine 自研存储引擎

AriaEngine 是内置的页面式存储引擎,对标 SQLite 的设计理念: LSM-Tree 索引 + WAL 崩溃恢复 + MVCC 事务 + 页面化物理存储 + 全库加密

const db = await MetonaSqlark.create({
  name: 'my-app',
  mode: 'aria',
  diskEngine: 'opfs',         // 存储后端:'opfs' | 'kv' | 'memory'
  aria: {
    walSyncMode: 'full',      // 'full' | 'batch' | 'none'
    compression: true,        // LZ4 页面压缩
    encryption: { password: 'my-password' },  // 全库 AES-256-GCM 加密
  },
});

// 与其余引擎 API 完全兼容
await db.defineTable('users', {
  id: { type: 'string', primaryKey: true },
  name: { type: 'string', required: true },
});
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
const rows = await db.query('SELECT * FROM users');

架构

┌──────────────────────────────────────────────┐
│              AriaEngine v0.6.1               │
├──────────────────────────────────────────────┤
│  LSM-Tree      │  Buffer Pool   │    WAL     │
│  MemTable      │  LRU (256页)   │  分片文件   │
│  +SSTable      │  +FileManager  │  +CRC-32   │
│ (4KB 页面)     │               │  +空洞检测  │
├────────────────┼───────────────┼────────────┤
│  MVCC 事务     │  Bloom Filter │  LZ4 压缩  │
│  快照隔离      │  二级索引 LSM │  +大小头    │
│  +Savepoint    │  (每列独立)   │            │
├────────────────┴───────────────┴────────────┤
│  EncryptedBackend (AES-256-GCM 全库透明加密) │
├──────────────────────────────────────────────┤
│  存储后端: OPFS Backend / KVStore Backend    │
│  Web Locks 多标签页独占锁                    │
└──────────────────────────────────────────────┘

核心机制

机制 说明
LSM-Tree MemTable(红黑树)→ 多级 SSTable,异步 Compaction(从存储兜底加载),写背压
页面化存储 SSTable 存为 4KB 页面(FileManager 分配 pageId + BufferPool LRU 缓存 256 页 ≈ 1MB),pageStorage 在 opfs/kv 后端默认启用
WAL 分片文件 __wal_%06d.bin + 真追加;标准 CRC32 记录校验;full/batch/none 三模式;空洞检测截断;16MB 阈值自动 checkpoint(活跃事务期间不截断)
崩溃恢复 打开时完整性校验(损坏 SSTable 自愈清理 + 整文件 CRC-32)、WAL 恢复、恢复后自动重建二级索引;repair() 清理孤儿页面与残留
全库加密 encryption.password → EncryptedBackend 透明加解密(WAL/SSTable/Schema/元数据全密文);PBKDF2 派生 + salt 持久化;密码错误/篡改 → ARIA_DECRYPT_ERROR
MVCC 版本链 + 快照隔离,事务读写不互斥,自动 GC
二级索引 每列独立 LSM Tree,支持等值/范围扫描,跨重启恢复,WAL 恢复后自动重建
Bloom Filter FNV-1a + Murmur 双哈希,SSTable footer 序列化,查询时快速否定
多标签页锁 Web Locks 库级独占锁,第二个标签页抛 ARIA_LOCKED;不支持的环境降级无锁并告警
维护语句 ANALYZE(表统计)/ REINDEX(重建索引)/ VACUUM(压缩 + MVCC GC/ EXPLAIN(查询计划)

AriaEngine 配置项

属性 类型 默认 说明
pageSize number 4096 页面大小(字节)
bufferPoolPages number 256 Buffer Pool 页面数(≈1MB
memtableSizeThreshold number 4MB MemTable 刷盘阈值
levelSizeMultiplier number 10 LSM 层级容量倍数
bloomFilterBitsPerKey number 10 Bloom Filter 每 key 位数
walEnabled boolean true 是否启用 WAL
walSyncMode 'full' | 'batch' | 'none' 'full' WAL 同步模式
checkpointInterval number 1000 Checkpoint 间隔(操作数)
walSizeThreshold number 16MB WAL 大小阈值(超则强制 checkpoint)
compression boolean false 是否启用 LZ4 压缩
storageBackend 'opfs' | 'kv' | 'memory' 'opfs' 存储后端
encryption { password: string } 全库 AES-256-GCM 加密
pageStorage boolean 自动(opfs/kv 后端默认启用) SSTable 页面化存储

框架集成

// React
import { useQuery, useTable, useDatabase } from '@metona-team/metona-sqlark/react';
const { data, loading, error, refresh } = useQuery(db, 'SELECT * FROM users');

// Vue
import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '@metona-team/metona-sqlark/vue';
const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');

开发

npm install       # 安装依赖
npm run dev       # 开发模式(localhost:3001
npm run build     # 生产构建(生成 dist/
npm test          # 运行测试(1304 用例 · 76 套件)
npm run test:e2e  # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint      # 代码检查
npm run typecheck # 类型检查

项目状态

指标 数值
测试用例 1304+12 Playwright e2e
测试套件 76
行覆盖率 90.1%
SQL 关键字 72
存储引擎 5Memory / KVStore / OPFS / Hybrid / Aria
运行时依赖 0

已知限制(v0.7.4

  • 单列主键 — 复合主键暂不支持(建表时显式 SCHEMA_ERROR),列入 v0.8 路线图
  • 写语句关联引用 — UPDATE/DELETE 的 WHERE 支持非关联子查询(IN (SELECT) / 标量子查询),关联引用($col / 关联 EXISTS)显式抛 NOT_SUPPORTED(不静默)
  • 建表 UNIQUE 约束 — 不可经 DROP INDEX 解除(对齐 SQLite,需重建表);仅 CREATE UNIQUE INDEX 添加的约束可随索引删除
  • 唯一值交换更新 — 同一语句内两行互换唯一列值(A:x→y, B:y→x)保守拒绝(最终状态合法但报 UNIQUE_VIOLATION
  • 主键非空 — 主键列强制非空(SQL 语义 PK 隐含 NOT NULL),INSERT/UPDATE 置 null/undefined 抛 VALIDATION_ERROR

浏览器兼容性

浏览器 最低版本 Memory Disk (KVStore) Aria (OPFS)
Chrome / Edge 102+
Firefox 111+
Safari 15.2+
Node.js 16+ (内存介质) (内存介质)

OPFS:基础 APIcreateWritable 原子写)在 Chromium 102+ / Firefox 111+ / Safari 15.2+ 均支持。 无跨文件事务,AriaEngine 以 WAL 分片单文件原子写 + 空洞检测截断保证崩溃一致性。

多标签页保护AriaEngine 通过 Web Locks 获取库级独占锁,第二个标签页打开同一库抛 ARIA_LOCKED


项目结构

src/
├── index.ts              # 入口(MetonaSqlark + MeSqlark
├── core.ts               # 主类(create/query/table/transaction/migration...
├── constants.ts          # 配置类型 + DatabaseError + VERSION
├── connection-manager.ts # 连接池管理
├── engine/               # 存储引擎
│   ├── memory.ts         #   MemoryEngine(内存 + 快照事务)
│   ├── kvstore_engine.ts #   KVStoreEnginedisk 模式,v0.6.0 替代 IndexedDB
│   ├── kvstore/          #   自研 KVStore(日志 + 快照 + 原子写)
│   └── aria/             #   AriaEngineLSM-Tree + WAL + MVCC + 页面化 + 加密)
│       ├── index/        #     MemTable / SSTable / LSM / Bloom / MergeIterator
│       ├── wal/          #     WAL 分片 + checkpoint
│       ├── store/        #     OPFS / KVStore / 加密 backend + FileManager
│       ├── buffer/       #     BufferPool + LRU 驱逐
│       └── transaction/  #     MVCC
├── hybrid/               # 混合引擎(write-through
├── migration/            # 旧 IndexedDB 数据迁移工具(一次性)
├── query/                # AST + Builder + Compiler + Executor
├── sql/                  # Lexer + Parser(递归下降,72 关键字)
├── table/                # 表管理 + Schema 校验
├── transaction/          # 事务管理(自动回滚)
├── plugin/               # 插件系统(14 hooks
└── integrations/         # React / Vue hooks

License

MIT © MetonaTeam

S
Description
Frontend TypeScript SQL database with dual-mode storage
https://sqlark.metona.cn/
Readme MIT
14 MiB
Languages
TypeScript 90%
HTML 9.6%
JavaScript 0.4%