release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
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:
@@ -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
@@ -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 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); 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 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); 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 → ROLLBACK(a3 不会生效)
|
||||
BEGIN;
|
||||
INSERT INTO audit VALUES ('a3', 'will be rolled back');
|
||||
ROLLBACK;
|
||||
SELECT * FROM audit ORDER BY id;
|
||||
|
||||
-- 事务语句:BEGIN → COMMIT(a4 生效)
|
||||
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
@@ -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"><!-- UMD 格式,暴露 window.MetonaSqlark 和 window.MeSqlark --></span>
|
||||
<script src=<span class="s">"https://git.metona.cn/.../metona-sqlark.min.js"</span>></script>
|
||||
<script src=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"</span>></script>
|
||||
<script>
|
||||
<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
@@ -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-shakable,UMD/ESM/CJS 多格式输出,~10KB gzip。</p>
|
||||
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakable,UMD/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>
|
||||
|
||||
Reference in New Issue
Block a user