用户报告"站点演示失败了",实测复现并定位根因: - 现象:直接双击 site/demo.html(file://)→ 点「🌲 Aria」→ "❌ 数据库初始化失败: Failed to open AriaEngine database "demo""(Memory 正常)。 - 根因:file:// 属不透明来源,Chromium 拒绝 navigator.storage.getDirectory() 并抛 SecurityError;此时 isSecureContext 仍为 true、API 也存在,无法提前探测。 引擎把它包成 ARIA_OPEN_ERROR 时丢掉了底层错误 → 消息对用户不可操作。 - 修复:OPFSBackend.open() 显式检查并抛 ARIA_OPFS_UNAVAILABLE,消息给出两条出路 (用 http(s) 打开 / 改用 mode:'memory'),原始 SecurityError 挂 cause; site/demo.html 额外用中文说明"为什么失败 + 怎么修"。 顺带修掉一个更普遍的问题:DatabaseError 的第三个参数只进 details,err.cause 恒为 undefined,而文档/注释多处写"底层错误作为 cause 保留"。现在两者都成立 (details 语义不变;cause 声明为公开字段并接入标准错误链)。 站点版本同步:demo.html(title / 状态栏 / SQL 预置脚本 / console 日志)与 benchmark.html(title)此前仍是 v0.7.4(日志甚至是 v0.4.2)→ 统一 v0.8.0; docs.html 的 AriaEngine 版本演进列表补上 v0.8.0 条目、错误码表补 ARIA_OPFS_UNAVAILABLE;README 补"OPFS 需要 http(s) 页面"的浏览器兼容说明。 回归与门禁:tests/engine/aria-opfs-unavailable.test.ts(5 项,含正常环境正控); 变异 R19 / R20 均被拦住(总计 42/42);93 套件 / 1985 用例;覆盖率 90.59 / 82.61 / 94.14 / 93.50(阈值 90/82/94/93);e2e 14/14;lint + 两份 tsc 干净; dist 重建(251,731 B / gzip 63,431 B)并已同步全部体积宣称。
264 lines
11 KiB
Markdown
264 lines
11 KiB
Markdown
# Contributing to MetonaSqlark
|
||
|
||
Thanks for your interest in contributing! This document outlines the development workflow and conventions.
|
||
|
||
## Prerequisites
|
||
|
||
- Node.js >= 16.0.0
|
||
- npm >= 8.0.0
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
git clone https://git.metona.cn/MetonaTeam/MetonaSqlark.git
|
||
cd MetonaSqlark
|
||
npm install
|
||
```
|
||
|
||
## Development
|
||
|
||
```bash
|
||
# Start dev server with hot reload (port 3001)
|
||
npm run dev
|
||
|
||
# Run tests in watch mode
|
||
npm run test:watch
|
||
|
||
# Type check
|
||
npm run typecheck
|
||
|
||
# Lint
|
||
npm run lint
|
||
npm run lint:fix
|
||
|
||
# Format
|
||
npm run format
|
||
```
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
src/
|
||
├── index.ts # Entry point, global API (MetonaSqlark + MeSqlark)
|
||
├── core.ts # MetonaSqlark main class
|
||
├── constants.ts # Types, defaults, enums, errors
|
||
├── connection-manager.ts # Connection pool (connect/disconnect)
|
||
├── engine/ # Storage engines
|
||
│ ├── interface.ts # IStorageEngine interface
|
||
│ ├── memory.ts # MemoryEngine (Map-based)
|
||
│ ├── kvstore_engine.ts # KVStoreEngine (disk mode, self-built KV store)
|
||
│ ├── kvstore/ # KVStore (log + snapshot + atomic multi-key write)
|
||
│ └── 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 (OPFS / KVStore / Memory / Encrypted)
|
||
│ ├── locks.ts # Web Locks multi-tab exclusive lock
|
||
│ └── compression/ # LZ4
|
||
├── hybrid/ # HybridEngine (write-through)
|
||
├── migration/ # Legacy IndexedDB migration tool (one-shot)
|
||
├── table/ # Table management & Schema validation
|
||
├── query/ # Query system
|
||
│ ├── ast.ts # SQL AST type definitions
|
||
│ ├── builder.ts # Chainable QueryBuilder API
|
||
│ ├── compiler.ts # AST → QueryPlan compiler
|
||
│ ├── executor.ts # QueryExecutor with JOIN/GROUP BY support
|
||
│ └── where-matcher.ts # Unified WHERE matching logic
|
||
├── sql/ # SQL parser
|
||
│ ├── tokens.ts # Token types & keywords
|
||
│ ├── lexer.ts # Tokenizer
|
||
│ ├── parser.ts # Recursive descent parser
|
||
│ └── params.ts # Parameter binding (? placeholders)
|
||
├── transaction/ # Transaction manager
|
||
├── plugin/ # Plugin system (14 lifecycle hooks)
|
||
└── integrations/ # React & Vue hooks
|
||
|
||
tests/ # Test suite (1985 test cases, 92 suites + 14 e2e)
|
||
tests/helpers/ # 共享测试工具(OPFS mock 等)
|
||
tests/e2e/ # Playwright e2e(真实 Chromium + OPFS)
|
||
site/ # Documentation site (index / docs / demo)
|
||
```
|
||
|
||
## Code Conventions
|
||
|
||
### TypeScript
|
||
- **Strict mode** is enabled — all code must pass `tsc --noEmit`.
|
||
- Export types explicitly. Avoid `any` where possible.
|
||
- Use `interface` for object shapes, `type` for unions/primitives.
|
||
|
||
### Style
|
||
- Run `npm run format` before committing (uses Prettier).
|
||
- Follow existing comment patterns: JSDoc `/** */` for public APIs, `//` for inline notes.
|
||
- Keep functions focused and under ~60 lines where practical.
|
||
|
||
### Testing
|
||
- **Every new feature must include tests.**
|
||
- Test files mirror source structure: `src/foo.ts` → `tests/foo.test.ts`.
|
||
- Use descriptive test names: `('does X when Y')`.
|
||
- Run the full suite before submitting: `npm test`.
|
||
|
||
### Commits
|
||
- Use conventional commit messages:
|
||
- `feat: add SQL GROUP BY support`
|
||
- `fix: JOIN with multiple ON conditions`
|
||
- `docs: update API reference`
|
||
- `test: add edge case coverage`
|
||
- `chore: optimize rollup dev build`
|
||
|
||
## Architecture
|
||
|
||
MetonaSqlark follows a layered architecture:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ MetonaSqlark / MeSqlark (Public API) │
|
||
├──────────────────────────────────────────────────┤
|
||
│ SQL Parser │ QueryBuilder │ Transaction │ Plugin │
|
||
│ (Lexer→AST) │ (.select…) │ Manager │ System │
|
||
├──────────────────────────────────────────────────┤
|
||
│ QueryExecutor (AST → Results) │
|
||
│ JOIN · GROUP BY · HAVING · DISTINCT · Agg │
|
||
├──────────────────────────────────────────────────┤
|
||
│ IStorageEngine (Interface) │
|
||
├─────────┬──────────┬──────────┬──────────────────┤
|
||
│ Memory │ IndexedDB │ OPFS │ Hybrid │
|
||
│ Engine │ Engine │ Engine │ Engine │
|
||
└─────────┴──────────┴──────────┴──────────────────┘
|
||
```
|
||
|
||
### Key Design Decisions
|
||
|
||
1. **AST as Universal Intermediate Representation**: Both SQL strings and Query Builder produce the same AST, ensuring consistent behavior regardless of API used.
|
||
|
||
2. **Write-Through Hybrid Strategy**: When using `mode: 'hybrid'`, all writes go to both memory and disk simultaneously. Reads always hit memory for maximum speed.
|
||
|
||
3. **Plugin Hook Pipeline**: 14 lifecycle hooks let plugins observe and adjust database
|
||
operations without modifying core code. **Hook contract (v0.8.0)**:
|
||
- return values are **ignored** (you cannot cancel an operation by returning `false`,
|
||
nor rewrite the SQL by returning a string);
|
||
- **mutate the argument object in place** to change it — this works on the Table API
|
||
path (`db.table(...).insert(rows)`);
|
||
- **throw** to abort the operation (the error propagates to the caller);
|
||
- the SQL path passes a **copy** for `beforeInsert`, so mutating it there has no effect.
|
||
Use the Table API when you need to transform rows.
|
||
These rules are covered by tests; changing them requires updating this section.
|
||
|
||
4. **Hand-Written SQL Parser**: No dependencies on parser generators — a recursive-descent parser keeps the bundle size minimal.
|
||
|
||
## Building
|
||
|
||
```bash
|
||
# Production build (all formats)
|
||
npm run build
|
||
|
||
# Output in dist/
|
||
# ├── metona-sqlark.js UMD
|
||
# ├── metona-sqlark.min.js UMD minified (251,731 B / gzip 63,431 B)
|
||
# ├── metona-sqlark.esm.js ES Module
|
||
# ├── metona-sqlark.cjs CommonJS
|
||
# └── metona-sqlark.d.ts TypeScript declarations
|
||
```
|
||
|
||
## Plugin Development
|
||
|
||
Plugins follow a simple convention:
|
||
|
||
```typescript
|
||
import type { MetonaPlugin } from '@metona-team/metona-sqlark';
|
||
|
||
const myPlugin: MetonaPlugin = {
|
||
name: 'myPlugin',
|
||
version: '1.0.0',
|
||
description: 'Description of my plugin',
|
||
// higher = executed first(v0.8.0 起真正生效:install 与钩子都按优先级降序;
|
||
// 同优先级保持 plugins 数组顺序)
|
||
priority: 50,
|
||
|
||
install(db) {
|
||
// Use db.on() to subscribe to hooks
|
||
db.on('beforeInsert', async (rows) => {
|
||
// Validate or transform data
|
||
});
|
||
db.on('afterQuery', async (sql, result) => {
|
||
// Log or cache query results
|
||
});
|
||
},
|
||
|
||
destroy() {
|
||
// Clean up event listeners, timers, etc.
|
||
},
|
||
};
|
||
|
||
// Register in config
|
||
const db = await MetonaSqlark.create({
|
||
name: 'my-app',
|
||
plugins: [myPlugin],
|
||
});
|
||
```
|
||
|
||
### Available Lifecycle Hooks
|
||
|
||
| Hook | Trigger | Parameters |
|
||
|------|---------|------------|
|
||
| `beforeCreateTable` | Before table creation | schema |
|
||
| `afterCreateTable` | After table creation | schema |
|
||
| `beforeDropTable` | Before table drop | tableName |
|
||
| `afterDropTable` | After table drop | tableName |
|
||
| `beforeInsert` | Before row insert | rows[] |
|
||
| `afterInsert` | After row insert | rows[] |
|
||
| `beforeUpdate` | Before row update | query, updates |
|
||
| `afterUpdate` | After row update | query, updates, count |
|
||
| `beforeDelete` | Before row delete | query |
|
||
| `afterDelete` | After row delete | query, count |
|
||
| `beforeQuery` | Before SQL query | sql |
|
||
| `afterQuery` | After SQL query | sql, result |
|
||
| `beforeTransaction` | Before transaction | - |
|
||
| `afterTransaction` | After transaction | - |
|
||
|
||
## Releasing
|
||
|
||
1. Update version in `package.json` and `src/constants.ts` (`VERSION` constant).
|
||
2. Update `CHANGELOG.md`.
|
||
3. Run full test suite: `npm test`.
|
||
4. Build: `npm run build`.
|
||
5. Publish: `npm publish`.
|
||
|
||
## Questions?
|
||
|
||
Open an issue at [git.metona.cn/MetonaTeam/MetonaSqlark/issues](https://git.metona.cn/MetonaTeam/MetonaSqlark/issues).
|
||
|
||
---
|
||
|
||
## 变异验证(回归套件的"是否只是陪跑"检查)
|
||
|
||
修复类提交必须能回答一个问题:**把修复回退到修复前的行为,对应用例会不会失败?**
|
||
不会失败的用例等于没有保护。
|
||
|
||
```bash
|
||
# 全部 42 项变异(存储单一提交点 / LSM 结构根治 / v0.8.0 全量审查修复)
|
||
python3 scripts/mutation-b6.py
|
||
|
||
# 只跑其中一条(按名字子串匹配)
|
||
python3 scripts/mutation-b6.py "WAL 分片号复用"
|
||
```
|
||
|
||
脚本会临时改写 `src/`、运行对应用例、再恢复源码(收到 SIGINT/SIGTERM 也会恢复),
|
||
最后打印每一条是"被拦住"还是"仍然通过"。**出现任何一条"仍然通过",本次提交不算完成。**
|
||
|
||
### 脚本自身的保证(v0.8.0 审查后加固)
|
||
|
||
| 机制 | 为什么需要 |
|
||
| --- | --- |
|
||
| **正控** | 先跑一遍未变异的干净基线(B-6 套件 + 分片 WAL 套件必须全绿)。没有正控就无法区分"变异被测试拦住"与"这套件本来就是红的" |
|
||
| **编译失败 ≠ 被拦住** | `Test suite failed to run` / `error TS####` 单独归类为 `BAD(变异破坏编译)`:一个把源码改成语法错误的"变异"什么都没验证 |
|
||
| **用例未匹配 ≠ 被拦住** | `Tests: 0 total` / `No tests found` 归类为 `BAD(用例未匹配)`(模式写错时最容易被误读成"测试通过了") |
|
||
| **锚点唯一性** | 锚点在文件里出现多次时拒绝变异(否则可能改到另一处、验错位置) |
|
||
| **超时** | 单条变异 300s 上限,超时归类为 `BAD(超时)` 而不是放过 |
|
||
| **逐字节恢复校验** | 每条变异跑完用 sha256 比对恢复结果;不一致立即 `FATAL` 退出 |
|
||
| **进程锁** | `.mutation-b6.lock`(`O_EXCL`):并发运行会互相踩掉对方的源码,直接拒绝启动 |
|
||
|
||
判定顺序固定为:编译失败 → 用例未匹配 → `✕`/`Tests: N failed`(= 被拦住)→ 其余为"仍然通过"。
|
||
只有 `OK(变异被拦住)` 计入成功,其余任何状态都会让脚本以非零退出码结束。
|