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
+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ 性能基准 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
--primary:#6366f1; --accent:#06b6d4; --accent2:#ec4899; --green:#22c55e;
--text:#e2e8f0; --text2:#8b949e; --radius:12px;
--gradient:linear-gradient(135deg,#6366f1,#06b6d4,#ec4899);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
header {
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
background:var(--surface); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10;
}
.logo { font-weight:800; font-size:1.1rem; display:flex; align-items:center; gap:10px; }
.logo .icon { width:28px; height:28px; border-radius:7px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:900; color:#fff; }
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:20px; }
nav a { color:var(--text2); text-decoration:none; font-size:0.88rem; transition:.2s; }
nav a:hover,.nav-active { color:var(--text); }
.container { max-width:1000px; margin:0 auto; padding:32px 24px 60px; }
.hero { margin-bottom:28px; }
.hero h1 { font-size:1.6rem; margin-bottom:8px; }
.hero p { color:var(--text2); font-size:0.92rem; line-height:1.7; }
.hero p code { background:var(--surface2); padding:2px 6px; border-radius:5px; font-size:0.85em; }
.controls { display:flex; gap:12px; align-items:center; margin-bottom:24px; flex-wrap:wrap; }
.btn {
display:inline-flex; align-items:center; gap:6px; padding:10px 22px; border-radius:8px;
font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
}
.btn-run { background:var(--gradient); color:#fff; box-shadow:0 4px 15px rgba(99,102,241,0.35); }
.btn-run:hover { transform:translateY(-1px); box-shadow:0 6px 25px rgba(99,102,241,0.5); }
.btn-run:active { transform:scale(0.97); }
.btn-run:disabled { opacity:0.5; cursor:not-allowed; transform:none; }
.btn-small { background:var(--surface2); color:var(--text2); border:1px solid var(--border); font-size:0.8rem; padding:8px 16px; }
.btn-small:hover { color:var(--text); border-color:var(--text2); }
select {
background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:8px;
padding:9px 12px; font-size:0.85rem; font-family:inherit; outline:none;
}
.status { font-size:0.85rem; color:var(--text2); }
.status .spinner { display:inline-block; width:14px; height:14px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; vertical-align:-2px; }
@keyframes spin { to { transform:rotate(360deg); } }
table { width:100%; border-collapse:collapse; background:var(--surface); border-radius:var(--radius); overflow:hidden; border:1px solid var(--border); }
th,td { padding:12px 16px; text-align:left; font-size:0.88rem; border-bottom:1px solid var(--border); }
th { background:var(--surface2); color:var(--text2); font-size:0.78rem; text-transform:uppercase; letter-spacing:0.8px; white-space:nowrap; }
tr:last-child td { border-bottom:none; }
td.num { font-family:'JetBrains Mono',monospace; text-align:right; }
td.best { color:var(--green); font-weight:700; }
.group-header td { background:rgba(99,102,241,0.08); font-weight:700; color:var(--accent); }
.section { margin-bottom:32px; }
.section h2 { font-size:1.05rem; margin-bottom:14px; color:var(--text); display:flex; align-items:center; gap:8px; }
.section h2 .tag { font-size:0.68rem; padding:3px 8px; border-radius:999px; background:var(--surface2); border:1px solid var(--border); color:var(--text2); font-weight:600; }
.note { font-size:0.8rem; color:var(--text2); margin-top:14px; line-height:1.7; }
.note code { background:var(--surface2); padding:2px 5px; border-radius:4px; }
.summary-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:28px; }
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:18px; }
.card .val { font-size:1.5rem; font-weight:800; background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.card .lbl { font-size:0.78rem; color:var(--text2); margin-top:4px; }
</style>
</head>
<body>
<header>
<div class="logo"><div class="icon"></div><span>MetonaSqlark</span></div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html" class="nav-active">基准</a>
</nav>
</header>
<div class="container">
<div class="hero">
<h1>⚡ 性能基准测试</h1>
<p>在浏览器中实测 <code>Memory</code><code>Aria</code>(LSM-Tree 自研引擎)两种模式的真实性能。
数据量分三档(1K / 10K / 50K 行),测量 <code>INSERT</code> / 主键查询 / 索引查询 / <code>UPDATE</code> / <code>DELETE</code> 的吞吐(ops/sec)。
<strong>测试结果取决于你的设备与浏览器</strong>,仅供参考。</p>
</div>
<div class="controls">
<button class="btn btn-run" id="run-btn" onclick="runAll()">▶ 运行完整基准</button>
<button class="btn btn-small" onclick="runAll(true)">⚡ 快速模式(1K/10K</button>
<button class="btn btn-small" onclick="window.location.reload()">↻ 重置</button>
<span class="status" id="status"></span>
</div>
<div class="summary-cards" id="summary"></div>
<div class="section" id="result-section" style="display:none">
<h2>测试结果 <span class="tag">ops/sec</span></h2>
<table id="result-table">
<thead><tr><th>引擎</th><th>数据量</th><th>INSERT</th><th>主键查询</th><th>索引查询</th><th>UPDATE</th><th>DELETE</th></tr></thead>
<tbody></tbody>
</table>
<p class="note">注:Memory 模式为纯内存 Map 存储;Aria 模式为 LSM-Tree + MemTable 存储(<code>storageBackend: 'memory'</code>,无磁盘 I/O)。
INSERT 为单行批量写入(含 WAL 记录);UPDATE/DELETE 按主键条件执行。</p>
</div>
</div>
<script src="../dist/metona-sqlark.js"></script>
<script>
const M = window.MetonaSqlark;
const SIZES = [1000, 10000, 50000];
const SIZES_FAST = [1000, 10000];
let cancelled = false;
function setStatus(text, spin) {
const el = document.getElementById('status');
el.innerHTML = (spin ? '<span class="spinner"></span> ' : '') + text;
}
function setRunning(on) {
document.getElementById('run-btn').disabled = on;
}
function makeRows(n) {
const rows = [];
for (let i = 0; i < n; i++) {
rows.push({ id: 'k' + i, name: 'User' + i, age: 18 + (i % 60), city: 'City' + (i % 100) });
}
return rows;
}
async function createDb(mode) {
const db = new M.MetonaSqlark({
name: 'bench-' + mode + '-' + Date.now(),
mode,
diskEngine: 'memory',
});
await db.init();
await db.defineTable('bench', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number', index: true },
city: { type: 'string' },
});
return db;
}
async function timeIt(fn) {
const start = performance.now();
await fn();
return performance.now() - start;
}
async function runBench(mode, size) {
const db = await createDb(mode);
const rows = makeRows(size);
const result = { mode, size, insert: 0, pk: 0, idx: 0, update: 0, del: 0 };
try {
// INSERT(批量)
let ms = await timeIt(() => db.table('bench').insertMany(rows));
result.insert = Math.round(size / (ms / 1000));
// 主键查询 × 500
ms = await timeIt(async () => {
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE id = 'k${(i * 37) % size}'`);
});
result.pk = Math.round(500 / (ms / 1000));
// 索引查询 × 500
ms = await timeIt(async () => {
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE age = ${18 + ((i * 17) % 60)}`);
});
result.idx = Math.round(500 / (ms / 1000));
// UPDATE × 200(按主键)
ms = await timeIt(async () => {
for (let i = 0; i < 200; i++) await db.query(`UPDATE bench SET name = 'X${i}' WHERE id = 'k${(i * 53) % size}'`);
});
result.update = Math.round(200 / (ms / 1000));
// DELETE × 200(按主键)
ms = await timeIt(async () => {
for (let i = 0; i < 200; i++) await db.query(`DELETE FROM bench WHERE id = 'k${(i * 71) % size}'`);
});
result.del = Math.round(200 / (ms / 1000));
} finally {
await db.close();
}
return result;
}
async function runAll(quick) {
if (cancelled) { cancelled = false; }
const sizes = quick ? SIZES_FAST : SIZES;
const btn = document.getElementById('run-btn');
btn.textContent = '⏳ 测试中…';
setRunning(true);
document.getElementById('result-section').style.display = 'none';
document.getElementById('summary').innerHTML = '';
const results = [];
for (const mode of ['memory', 'aria']) {
for (const size of sizes) {
setStatus(`正在测试 ${mode === 'memory' ? 'Memory' : 'Aria'} 引擎 · ${size.toLocaleString()} 行…`, true);
// 让 UI 刷新
await new Promise((r) => setTimeout(r, 30));
try {
const r = await runBench(mode, size);
results.push(r);
} catch (e) {
setStatus(`${mode}/${size} 失败: ${e.message}`, false);
console.error(e);
}
}
}
renderResults(results);
setStatus('✓ 测试完成', false);
btn.textContent = '▶ 运行完整基准';
setRunning(false);
}
function renderResults(results) {
const tbody = document.querySelector('#result-table tbody');
tbody.innerHTML = '';
// 汇总卡片
const summary = document.getElementById('summary');
let bestInsert = 0, bestInsertMode = '', bestPk = 0, bestPkMode = '', totalTests = 0;
for (const r of results) {
totalTests++;
if (r.insert > bestInsert) { bestInsert = r.insert; bestInsertMode = r.mode; }
if (r.pk > bestPk) { bestPk = r.pk; bestPkMode = r.mode; }
}
summary.innerHTML = `
<div class="card"><div class="val">${results.length} 组</div><div class="lbl">测试组数</div></div>
<div class="card"><div class="val">${bestInsert.toLocaleString()} ops/s</div><div class="lbl">INSERT 峰值(${bestInsertMode}</div></div>
<div class="card"><div class="val">${bestPk.toLocaleString()} ops/s</div><div class="lbl">主键查询峰值(${bestPkMode}</div></div>
`;
// 表格(按引擎分组)
let lastMode = '';
for (const r of results) {
if (r.mode !== lastMode) {
lastMode = r.mode;
const row = document.createElement('tr');
row.className = 'group-header';
row.innerHTML = `<td colspan="6">${r.mode === 'memory' ? '🚀 Memory 引擎' : '🌲 Aria 引擎(LSM-Tree'}</td>`;
tbody.appendChild(row);
}
const row = document.createElement('tr');
const fmt = (v) => `<td class="num">${v ? v.toLocaleString() : '—'}</td>`;
row.innerHTML = `
<td>${r.mode}</td>
<td>${r.size.toLocaleString()} 行</td>
${fmt(r.insert)}${fmt(r.pk)}${fmt(r.idx)}${fmt(r.update)}${fmt(r.del)}
`;
tbody.appendChild(row);
}
document.getElementById('result-section').style.display = 'block';
document.getElementById('result-section').scrollIntoView({ behavior: 'smooth' });
}
</script>
</body>
</html>
+131 -9
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧪 在线演示 — MetonaSqlark v0.2.5</title>
<title>🧪 在线演示 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
@@ -82,14 +82,15 @@
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a>
</nav>
<div class="status"><span class="dot"></span> Memory 模式 — v0.2.5</div>
<div class="status"><span class="dot"></span> Memory 模式 — v0.3.2</div>
</header>
<div class="main">
<div class="editor-panel">
<div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.5 在线演示
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.3.2 在线演示
-- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
@@ -121,6 +122,12 @@
<button class="btn btn-preset" onclick="loadPreset('adv')">🧪 高级</button>
<button class="btn btn-preset" onclick="loadPreset('alter')">🏗 ALTER</button>
<button class="btn btn-preset" onclick="loadPreset('truncate')">🗑 TRUNCATE</button>
<button class="btn btn-preset" onclick="loadPreset('casewhen')">🎯 CASE WHEN</button>
<button class="btn btn-preset" onclick="loadPreset('union')">🔀 UNION</button>
<button class="btn btn-preset" onclick="loadPreset('insertselect')">📥 INSERT SELECT</button>
<button class="btn btn-preset" onclick="loadPreset('exists')">🔍 EXISTS</button>
<button class="btn btn-preset" onclick="loadPreset('index')">🗂 索引</button>
<button class="btn btn-preset" onclick="loadPreset('multistmt')">📜 多语句/事务</button>
<button class="btn btn-preset" onclick="loadPreset('aria')" style="color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</div>
</div>
@@ -511,9 +518,9 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理
DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.2.5)
aria: `-- 🌲 AriaEngine 演示 (v0.2.5 → v0.3.2)
-- AriaEngine: LSM-Tree 自研存储引擎
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 721测试
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 836测试
-- 基础 CRUD 完全兼容
CREATE TABLE IF NOT EXISTS tasks (
@@ -533,18 +540,133 @@ DROP TABLE temp_logs;`,
-- 聚合统计
SELECT done, COUNT(*) as cnt FROM tasks GROUP BY done;
-- CASE WHEN 表达式
SELECT title,
CASE WHEN done THEN '✅ done' ELSE '⏳ pending' END AS status
FROM tasks;
-- EXISTS 关联子查询
SELECT u.name FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- AriaEngine 特性:
-- • LSM-Tree: MemTable (红黑树) → SSTable 多级索引
-- • WAL: Write-Ahead Log 保证崩溃恢复
-- • WAL: Write-Ahead Log 保证崩溃恢复 + 批量组提交
-- • MVCC: 版本链 + 快照隔离
-- • Buffer Pool: LRU 页面缓存 (256页 ~ 1MB)
-- • Buffer Pool: SSTable LRU 缓存 (256页 ~ 1MB) ✅ 已生效
-- • Bloom Filter: FNV-1a + Murmur 双哈希
-- • Slotted Page: 4KB 页面 + Tuple 二进制编码
-- • 二级索引: 每列独立 LSM + 动态 CREATE INDEX
-- 生产环境: mode: 'aria' 激活自研引擎
-- const db = await MetonaSqlark.create({
-- name: 'my-app', mode: 'aria'
-- });`,
casewhen: `-- 🎯 CASE WHEN 条件表达式 (v0.3.1 / v0.3.2)
-- SELECT 列:多 WHEN + ELSE
SELECT name, age,
CASE WHEN age >= 30 THEN 'senior'
WHEN age >= 25 THEN 'mid'
ELSE 'junior' END AS age_group
FROM users ORDER BY age;
-- WHERE 条件中的 CASE WHEN
SELECT name FROM users
WHERE CASE WHEN age >= 25 THEN 'adult' ELSE 'young' END = 'adult';
-- 聚合中的 CASE WHEN
SELECT
SUM(CASE WHEN age >= 30 THEN 1 ELSE 0 END) AS seniors,
SUM(CASE WHEN age < 30 THEN 1 ELSE 0 END) AS juniors,
AVG(CASE WHEN age >= 25 THEN age END) AS avg_adult_age
FROM users;`,
union: `-- 🔀 UNION / UNION ALL (v0.3.0)
-- UNION 去重合并两表数据
SELECT name FROM users WHERE age >= 30
UNION
SELECT name FROM users WHERE age < 30;
-- UNION 对重复行去重(同查询两次 → 结果去重)
SELECT category FROM products
UNION
SELECT category FROM products;
-- UNION ALL 保留重复
SELECT category FROM products WHERE price >= 100
UNION ALL
SELECT category FROM products WHERE price >= 50;`,
insertselect: `-- 📥 INSERT INTO ... SELECT (v0.3.0)
-- 建备份表并复制全量数据
DROP TABLE IF EXISTS users_backup;
CREATE TABLE users_backup (
id STRING PRIMARY KEY, name STRING, email STRING, age NUMBER
);
INSERT INTO users_backup SELECT * FROM users;
-- 确认复制
SELECT COUNT(*) as backed_up FROM users_backup;
-- INSERT SELECT 带 WHERE 过滤
DROP TABLE IF EXISTS big_spenders;
CREATE TABLE big_spenders (
id STRING PRIMARY KEY, product STRING, amount NUMBER
);
INSERT INTO big_spenders (id, product, amount)
SELECT o.id, o.product, o.amount FROM orders o WHERE o.amount >= 150;
-- 确认过滤结果
SELECT * FROM big_spenders;`,
exists: `-- 🔍 EXISTS 关联子查询 (v0.3.0 / v0.3.1)
-- 有订单的用户(关联子查询)
SELECT u.name FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- 无订单的用户(NOT EXISTS
SELECT u.name FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- EXISTS + JOIN 组合
SELECT u.name, o.product, o.amount FROM users u
JOIN orders o ON u.id = o.user_id
WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 100);`,
index: `-- 🗂 动态索引 CREATE / DROP INDEX (v0.3.0)
-- 为 orders.user_id 创建索引
CREATE INDEX idx_orders_user ON orders (user_id);
-- 索引查找(走二级索引)
SELECT u.name, o.product, o.amount
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.user_id = '1';
-- 删除索引
DROP INDEX idx_orders_user ON orders (user_id);
-- 删除后回退全表扫描(结果不变)
SELECT * FROM orders WHERE user_id = '3';`,
multistmt: `-- 📜 多语句 + 事务语句 (v0.3.0)
-- 分号分隔的多语句一次执行
DROP TABLE IF EXISTS audit;
CREATE TABLE audit (id STRING PRIMARY KEY, msg STRING);
INSERT INTO audit VALUES ('a1', 'first');
INSERT INTO audit VALUES ('a2', 'second');
SELECT * FROM audit ORDER BY id;
-- 事务语句:BEGIN → ROLLBACKa3 不会生效)
BEGIN;
INSERT INTO audit VALUES ('a3', 'will be rolled back');
ROLLBACK;
SELECT * FROM audit ORDER BY id;
-- 事务语句:BEGIN → COMMITa4 生效)
BEGIN;
INSERT INTO audit VALUES ('a4', 'will be committed');
COMMIT;
SELECT COUNT(*) as total FROM audit;`,
};
function loadPreset(name) {
@@ -564,7 +686,7 @@ document.addEventListener('keydown', e => {
// Boot
initDB().then(() => {
console.log('✅ MetonaSqlark v0.2.5 demo ready');
console.log('✅ MetonaSqlark v0.3.2 demo ready');
setTimeout(runQuery, 300);
}).catch(err => {
renderError('初始化失败: ' + err.message);
+58 -6
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📖 API 文档 — MetonaSqlark v0.2.5</title>
<title>📖 API 文档 — MetonaSqlark v0.3.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
@@ -69,6 +69,7 @@
<a href="index.html">首页</a>
<a href="docs.html" class="nav-active">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
</nav>
</div>
</header>
@@ -121,7 +122,7 @@ npm install @metona-team/metona-sqlark</pre>
<h3>CDN / UMD</h3>
<pre><span class="c">&lt;!-- UMD 格式,暴露 window.MetonaSqlark 和 window.MeSqlark --&gt;</span>
&lt;script src=<span class="s">"https://git.metona.cn/.../metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;script src=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;script&gt;
<span class="k">const</span> db = <span class="k">await</span> window.<span class="f">MetonaSqlark</span>.create({...});
<span class="c">// 或 window.MeSqlark.create(...) — 完全等价</span>
@@ -138,9 +139,9 @@ npm install @metona-team/metona-sqlark</pre>
<table>
<tr><th>文件</th><th>格式</th><th>用途</th></tr>
<tr><td><code>metona-sqlark.js</code></td><td>UMD</td><td>浏览器开发版(含 sourcemap</td></tr>
<tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(~42KB / ~10KB gzip</td></tr>
<tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(~105KB / ~27KB gzip</td></tr>
<tr><td><code>metona-sqlark.esm.js</code></td><td>ES Module</td><td>现代打包工具 / 浏览器 ESM</td></tr>
<tr><td><code>metona-sqlark.cjs.js</code></td><td>CommonJS</td><td>Node.js require()</td></tr>
<tr><td><code>metona-sqlark.cjs</code></td><td>CommonJS</td><td>Node.js require()</td></tr>
<tr><td><code>metona-sqlark.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr>
</table>
@@ -237,6 +238,37 @@ db.<span class="f">isReady</span>(); <span class="c">// true</span>
<span class="c">-- TRUNCATE TABLE — 快速清空表数据 (v0.2.5)</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'TRUNCATE TABLE old_logs'</span>);</pre>
<h3>SQL 扩展 (v0.3.0+)</h3>
<pre><span class="c">// 多语句 — 分号分隔一次执行(返回最后一条结果)</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`CREATE TABLE t (id STRING PRIMARY KEY);
INSERT INTO t VALUES ('1'); INSERT INTO t VALUES ('2')`</span>);
<span class="c">// 事务语句 — BEGIN / COMMIT / ROLLBACK</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'BEGIN'</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO t VALUES ('3')"</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'ROLLBACK'</span>); <span class="c">// 回滚</span>
<span class="c">// INSERT INTO ... SELECT — 查询结果写入</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'INSERT INTO t SELECT id FROM t2 WHERE x > 1'</span>);
<span class="c">// UNION / UNION ALL — 合并查询</span>
<span class="k">const</span> merged = <span class="k">await</span> db.<span class="f">query</span>(
<span class="s">`SELECT name FROM users WHERE city = 'Beijing'
UNION SELECT name FROM users WHERE age < 30`</span>);
<span class="c">// EXISTS / NOT EXISTS — 关联子查询</span>
<span class="k">const</span> hasOrders = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`</span>);
<span class="c">// CASE WHEN — SELECT 列 / WHERE / 聚合 (v0.3.1 / v0.3.2)</span>
<span class="k">const</span> labeled = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT name,
CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END AS status FROM users`</span>);
<span class="k">const</span> adults = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END) FROM users`</span>);
<span class="c">// CREATE / DROP INDEX — 动态二级索引</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'CREATE INDEX idx_users_city ON users (city)'</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP INDEX idx_users_city ON users (city)'</span>);</pre>
<h3>条件表达式</h3>
<pre><span class="c">// 比较运算符</span>
<span class="s">`WHERE age > 18 AND name LIKE 'A%'`</span>
@@ -578,6 +610,25 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<span class="c">// 取消订阅</span>
<span class="f">unsubscribe</span>();</pre>
<h3>多标签页同步 (v0.3.2)</h3>
<pre><span class="c">// 启用 multiTabSync 后,其他标签页的写操作会广播到此标签页</span>
<span class="k">const</span> db = <span class="k">await</span> MetonaSqlark.<span class="f">create</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">mode</span>: <span class="s">'hybrid'</span>,
<span class="s">multiTabSync</span>: <span class="k">true</span>,
});
<span class="c">// 订阅其他标签页的变更(event.type === 'external'</span>
db.<span class="f">subscribe</span>(<span class="s">'users'</span>, (event) => {
<span class="k">if</span> (event.<span class="s">type</span> === <span class="s">'external'</span>) {
<span class="c">// Hybrid 模式已自动从磁盘重载,此处可刷新 UI</span>
<span class="f">refreshList</span>();
}
});
<span class="c">// 手动广播(Table API 已自动广播;自定义写入可调用)</span>
db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<h2 id="react">⚛️ React 集成</h2>
<pre><span class="k">import</span> { useQuery, useTable, useDatabase } <span class="k">from</span> <span class="s">'@metona-team/metona-sqlark/react'</span>;
<span class="k">import</span> { db } <span class="k">from</span> <span class="s">'./db'</span>;
@@ -663,6 +714,7 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<tr><td><code>onReady</code></td><td><code>(db) => void</code></td><td>-</td><td>初始化完成回调</td></tr>
<tr><td><code>onError</code></td><td><code>(err) => void</code></td><td>-</td><td>错误回调(v0.2.5 接入执行路径)</td></tr>
<tr><td><code>maxRowsPerQuery</code></td><td><code>number</code></td><td><code>0</code></td><td>查询结果行数上限(0=不限制)✅ v0.2.5 生效</td></tr>
<tr><td><code>multiTabSync</code></td><td><code>boolean</code></td><td><code>false</code></td><td>多标签页同步:BroadcastChannel 广播表变更,其他标签页自动刷新 🆕 v0.3.2</td></tr>
</table>
<h2 id="engine">💾 存储引擎</h2>
@@ -678,8 +730,8 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
<strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 701测试 · 零死代码。<br>
<strong>v0.2.5 质量加固</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 721测试 37套件。</p>
<strong>v0.2.4 生产级</strong> — 二级索引 · MVCC · BloomFilter · WAL CRC全同步 · AES-GCM加密 · Savepoint · EXPLAIN · ANALYZE · REINDEX · VACUUM · BufferPool · 761测试 · 零死代码。<br>
<strong>v0.3.2 表达式与并发</strong> — WAL full模式真正同步 · MVCC接入读写路径 · SSTableReader二分查找统一 · crypto实例化 · IndexedDB索引利用 · compactLevel public接口 · WAL大小阈值自动checkpoint · SQL注入防护 · ALTER TABLE · TRUNCATE TABLE · 761测试 41套件。</p>
<h3>存储模式对比</h3>
<table>
+7 -6
View File
@@ -143,6 +143,7 @@
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a>
</nav>
<a href="demo.html" class="btn btn-primary">立即体验</a>
@@ -152,7 +153,7 @@
<!-- Hero -->
<section class="hero">
<div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.5 质量加固 — 721测试 37套件 · 五模式全覆盖 · WAL同步修复 · ALTER/TRUNCATE · SQL注入防护 · 零回归</div>
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.3.2 表达式与并发 — 761测试 41套件 · 五模式全覆盖 · WAL同步修复 · ALTER/TRUNCATE · SQL注入防护 · 零回归</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions">
@@ -242,7 +243,7 @@ npm install @metona-team/metona-sqlark
</div>
<div class="feature-card">
<div class="icon">🌲</div>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.2.5</span></h3>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.3.2</span></h3>
<p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL full模式真正同步、MVCC 版本链接入读写路径。</p>
</div>
<div class="feature-card">
@@ -273,7 +274,7 @@ npm install @metona-team/metona-sqlark
<div class="feature-card">
<div class="icon">📦</div>
<h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~10KB gzip。</p>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~27KB gzip。</p>
</div>
<div class="feature-card">
<div class="icon">📊</div>
@@ -393,9 +394,9 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">721</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">91.0%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~10KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">761</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">81.0%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">36</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">37</div><div class="label">测试套件</div></div>