- chore: 升级 @metona-team/metona-sqlark 0.4.4 → 0.7.4,存储后端迁移 KVStore 引擎(内存索引+快照/日志,OPFS 落盘,jsdom 回退 memory), 启动时自动删除旧 IndexedDB 库(aria-MarkLiteV2 / Dexie MarkLite), 不做向下兼容 - fix: 保存竞态 — 快照比对后才清 isModified,防止保存期间的新输入被 误清标记导致永不落盘(自动保存与手动保存均修复) - fix: 另存为后标签重绑新路径(此前 Ctrl+S/自动保存仍写回旧文件), 并同步最近文件与快照持久化 - fix: tabSwitched 只依赖活动文件路径(此前每次击键都触发 IPC 并 重启主进程文件 watcher) - fix: 主进程关闭兜底超时 5s→12s(长于 confirm 10s,防强杀丢编辑) - fix: 外部修改检测覆盖非活动标签,banner 显示文件名, 标签有未保存修改时显式提示不再静默 - fix: 导入备份后 flush + closeDatabase 再 reload(防 OPFS 未落盘丢数据), 导入整体事务原子化(防半导入状态) - fix: 文档大纲跳过代码块内标题;切换标签恢复光标位置 - fix: 首次启动主题跟随系统偏好(settings 无记录时 load 返回 null) - fix: watcher error 恢复时重置 isSelfWriting,防外部修改通知被永久吞掉 - fix: 打开失败路径(最近文件/文件树/打开对话框)显式提示 - fix: 保存时保留原文件编码(UTF-16 LE/BE BOM 同编码写回) - feat: 多文件搜索 — dir:search IPC + SearchPanel 弹层 + Ctrl+Shift+F, 文件夹内递归搜索(大小写/正则,结果定位到行) - feat: 标签恢复时与磁盘 mtime 比对,自动同步磁盘最新内容 - test: 新增 outlineUtils(6) / searchInDir(7) / updateTabFilePath(2) 测试 - docs: README/DESIGN 同步 kv 后端与版本号 v0.6.2
837 lines
32 KiB
HTML
837 lines
32 KiB
HTML
<!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.7.4</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); height:100vh; overflow:hidden; }
|
||
header {
|
||
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
|
||
background:var(--surface); border-bottom:1px solid var(--border); z-index:10; position:relative;
|
||
}
|
||
.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); }
|
||
.status { display:flex; align-items:center; gap:8px; font-size:0.82rem; color:var(--text2); }
|
||
.status .dot { width:7px; height:7px; border-radius:50%; background:var(--green); animation:pulse 2s infinite; }
|
||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||
.main { display:flex; height:calc(100vh - 56px); }
|
||
.editor-panel { flex:1; display:flex; flex-direction:column; border-right:1px solid var(--border); min-width:400px; }
|
||
.result-panel { flex:1; display:flex; flex-direction:column; min-width:400px; }
|
||
.panel-header {
|
||
padding:12px 20px; font-size:0.78rem; font-weight:600; text-transform:uppercase;
|
||
letter-spacing:1px; color:var(--text2); border-bottom:1px solid var(--border);
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
}
|
||
.panel-header .badge {
|
||
font-size:0.7rem; padding:3px 10px; border-radius:999px; background:var(--surface2);
|
||
border:1px solid var(--border); text-transform:none; letter-spacing:0;
|
||
}
|
||
.editor-area { flex:1; display:flex; flex-direction:column; }
|
||
#sql-input {
|
||
flex:1; background:var(--surface); color:var(--text); border:none; padding:20px;
|
||
font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.9rem; line-height:1.8;
|
||
resize:none; outline:none; caret-color:var(--primary);
|
||
}
|
||
#sql-input::placeholder { color:var(--text2); opacity:0.5; }
|
||
.toolbar { display:flex; gap:8px; padding:12px 20px; border-top:1px solid var(--border); background:var(--surface2); flex-wrap:wrap; }
|
||
.btn {
|
||
display:inline-flex; align-items:center; gap:6px; padding:10px 20px; 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-clear { background:var(--surface2); color:var(--text2); border:1px solid var(--border); }
|
||
.btn-clear:hover { color:var(--text); border-color:var(--text2); }
|
||
.btn-preset { background:transparent; color:var(--accent); border:1px solid var(--accent); font-size:0.78rem; padding:6px 14px; }
|
||
.btn-preset:hover { background:rgba(6,182,212,0.12); }
|
||
.result-content { flex:1; overflow-y:auto; padding:20px; }
|
||
.result-table { width:100%; border-collapse:collapse; font-size:0.85rem; }
|
||
.result-table th { text-align:left; padding:10px 14px; background:var(--surface2); color:var(--accent); font-weight:600; border-bottom:2px solid var(--border); position:sticky; top:0; z-index:1; }
|
||
.result-table td { padding:8px 14px; border-bottom:1px solid var(--border); color:var(--text2); max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||
.result-table tr:hover td { background:rgba(99,102,241,0.04); }
|
||
.empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text2); gap:12px; }
|
||
.empty-state .icon { font-size:3rem; opacity:0.3; }
|
||
.error-box { background:rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); border-radius:10px; padding:16px; color:#f87171; font-family:'JetBrains Mono',monospace; font-size:0.82rem; margin-bottom:16px; }
|
||
.success-box { background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.3); border-radius:10px; padding:12px 16px; color:#4ade80; font-size:0.85rem; margin-bottom:16px; }
|
||
.sql-tag { display:inline-block; background:var(--surface2); border:1px solid var(--border); border-radius:6px; padding:4px 12px; font-family:'JetBrains Mono',monospace; font-size:0.75rem; color:var(--accent); margin-bottom:8px; }
|
||
::-webkit-scrollbar { width:6px; height:6px; }
|
||
::-webkit-scrollbar-track { background:transparent; }
|
||
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
|
||
@media(max-width:900px){ .main { flex-direction:column; } .editor-panel,.result-panel { min-width:auto; } .editor-panel { height:50%; } }
|
||
</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" class="nav-active">演示</a>
|
||
<a href="benchmark.html">基准</a>
|
||
</nav>
|
||
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.7.4</div>
|
||
<button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button>
|
||
<button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
|
||
</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.7.4 在线演示
|
||
-- 已预置 users / orders / products 表数据
|
||
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
|
||
|
||
-- 查看所有数据
|
||
SELECT * FROM users;
|
||
|
||
-- 条件查询 + 排序
|
||
SELECT name, age FROM users WHERE age > 25 ORDER BY age DESC;
|
||
|
||
-- JOIN 多表关联
|
||
SELECT u.name, o.product, o.amount
|
||
FROM users u INNER JOIN orders o ON u.id = o.user_id;
|
||
|
||
-- 聚合统计
|
||
SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
|
||
<div class="toolbar">
|
||
<button class="btn btn-run" onclick="runQuery()">▶ 执行 (Ctrl+Enter)</button>
|
||
<button class="btn btn-clear" onclick="clearResults()">🗑 清空</button>
|
||
<span style="flex:1"></span>
|
||
<button class="btn btn-preset" onclick="loadPreset('all')">📋 全数据</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('join')">🔗 JOIN</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('agg')">📊 聚合</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('ddl')">🏗 DDL</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('crud')">✏️ CRUD</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('subquery')">🔍 子查询</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('tx')">🔒 事务</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('scalar')">🎯 标量子查询</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('cascade')">🔗 级联</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('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('v040')" style="color:#22c55e;border-color:#22c55e;">🚰 v0.4.0 新特性</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('onupdate')" style="color:#fbbf24;border-color:#fbbf24;">🔄 ON UPDATE</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('repair')" style="color:#22c55e;border-color:#22c55e;">🛡 自愈</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('aria')" style="color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
|
||
<button class="btn btn-preset" onclick="loadPreset('maint')" style="color:#22c55e;border-color:#22c55e;">🛠 v0.5.1 维护语句</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="result-panel">
|
||
<div class="panel-header">
|
||
<span>📊 查询结果</span>
|
||
<span class="badge" id="row-count">0 行</span>
|
||
</div>
|
||
<div class="result-content" id="result-area">
|
||
<div class="empty-state">
|
||
<div class="icon">⚡</div>
|
||
<div>执行 SQL 查询以查看结果</div>
|
||
<div style="font-size:0.8rem;opacity:0.6">Ctrl + Enter 快捷执行 · 支持多语句</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="../dist/metona-sqlark.js"></script>
|
||
<script>
|
||
// Resolve global: try both MetonaSqlark and MeSqlark
|
||
const globalNs = window.MetonaSqlark || window.MeSqlark || {};
|
||
const { MetonaSqlark, MeSqlark, create } = globalNs;
|
||
const DBClass = MetonaSqlark || MeSqlark;
|
||
|
||
let db = null;
|
||
const resultArea = document.getElementById('result-area');
|
||
const rowCount = document.getElementById('row-count');
|
||
const sqlInput = document.getElementById('sql-input');
|
||
|
||
// Init database
|
||
let currentEngine = 'memory';
|
||
|
||
async function initDB() {
|
||
try {
|
||
const engine = currentEngine;
|
||
// 关闭旧实例(预设重置/引擎切换时数据回到初始状态)
|
||
if (db) { try { await db.close(); } catch { /* ignore */ } }
|
||
db = new DBClass({ name: 'demo', mode: engine });
|
||
await db.init();
|
||
|
||
// v0.4.2: Aria 引擎持久化 — 每次加载清空上次演示数据,保证演示确定性
|
||
if (engine === 'aria' && typeof db.getEngine().clearAll === 'function') {
|
||
await db.getEngine().clearAll();
|
||
}
|
||
|
||
await seedDemoData(db);
|
||
renderInfo(`✅ ${engine === 'aria' ? '🌲 AriaEngine(LSM-Tree + WAL + MVCC)' : '⚡ Memory'} 引擎已就绪`);
|
||
} catch (e) {
|
||
renderError('数据库初始化失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 建表 + 种子数据(每次预设重置/引擎切换后重建,保证演示可预期)
|
||
async function seedDemoData(db) {
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string', required: true },
|
||
email: { type: 'string' },
|
||
age: { type: 'number', default: 0 },
|
||
});
|
||
await db.defineTable('orders', {
|
||
id: { type: 'string', primaryKey: true },
|
||
user_id: { type: 'string' },
|
||
product: { type: 'string' },
|
||
amount: { type: 'number' },
|
||
quantity: { type: 'number', default: 1 },
|
||
});
|
||
await db.defineTable('products', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string', required: true },
|
||
price: { type: 'number', default: 0 },
|
||
category: { type: 'string' },
|
||
});
|
||
|
||
// Seed data
|
||
await db.table('users').insertMany([
|
||
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
|
||
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
|
||
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
|
||
{ id: '4', name: 'Diana', email: 'diana@demo.com', age: 28 },
|
||
{ id: '5', name: 'Eve', email: 'eve@demo.com', age: 22 },
|
||
]);
|
||
await db.table('orders').insertMany([
|
||
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200, quantity: 1 },
|
||
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50, quantity: 2 },
|
||
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150, quantity: 1 },
|
||
{ id: 'o4', user_id: '3', product: 'Monitor', amount: 400, quantity: 1 },
|
||
{ id: 'o5', user_id: '3', product: 'Cable', amount: 20, quantity: 3 },
|
||
]);
|
||
await db.table('products').insertMany([
|
||
{ id: 'p1', name: 'Laptop', price: 1200, category: 'Electronics' },
|
||
{ id: 'p2', name: 'Mouse', price: 50, category: 'Accessories' },
|
||
{ id: 'p3', name: 'Keyboard', price: 150, category: 'Accessories' },
|
||
]);
|
||
}
|
||
|
||
// v0.4.2: 切换存储引擎(重建数据库实例)
|
||
async function switchEngine(engine) {
|
||
if (engine === currentEngine) return;
|
||
currentEngine = engine;
|
||
document.getElementById('btn-memory').style.opacity = engine === 'memory' ? '1' : '0.6';
|
||
document.getElementById('btn-aria').style.opacity = engine === 'aria' ? '1' : '0.6';
|
||
document.getElementById('engine-status').textContent = engine === 'aria' ? '🌲 Aria' : '⚡ Memory';
|
||
document.getElementById('engine-dot').style.background = engine === 'aria' ? '#ec4899' : '';
|
||
renderInfo(`⏳ 正在初始化 ${engine === 'aria' ? 'Aria' : 'Memory'} 引擎…`);
|
||
await initDB();
|
||
}
|
||
|
||
// Run SQL
|
||
async function runQuery() {
|
||
const sql = sqlInput.value.trim();
|
||
if (!sql || !db) return;
|
||
|
||
const statements = sql.split(';').map(s => s.trim()).filter(s => {
|
||
// 过滤空语句和纯注释语句
|
||
if (s.length === 0) return false;
|
||
const nonComment = s.split('\n').filter(l => !l.trimStart().startsWith('--')).join('\n').trim();
|
||
return nonComment.length > 0;
|
||
});
|
||
|
||
try {
|
||
const allResults = [];
|
||
for (const stmt of statements) {
|
||
const result = await db.query(stmt);
|
||
// 深拷贝查询结果,防止后续语句通过引用修改已保存的结果
|
||
const resultCopy = Array.isArray(result) ? result.map(r => ({ ...r })) : result;
|
||
allResults.push({ sql: stmt, result: resultCopy });
|
||
}
|
||
|
||
if (allResults.length === 1) {
|
||
renderResult(allResults[0]);
|
||
} else {
|
||
renderAllResults(allResults);
|
||
}
|
||
} catch (e) {
|
||
renderError(e.message || String(e));
|
||
}
|
||
}
|
||
|
||
function renderResult({ sql, result }) {
|
||
if (Array.isArray(result)) {
|
||
renderTable(result);
|
||
} else {
|
||
renderSuccess(`✅ 执行成功 (影响 ${result ?? 0} 行)`);
|
||
}
|
||
}
|
||
|
||
function renderAllResults(allResults) {
|
||
let html = '';
|
||
for (const { sql, result } of allResults) {
|
||
html += `<div class="sql-tag">> ${escapeHTML(sql)}</div>`;
|
||
if (Array.isArray(result)) {
|
||
if (result.length === 0) {
|
||
html += '<div class="success-box">✅ 查询返回 0 行</div>';
|
||
} else {
|
||
html += buildTableHTML(result);
|
||
}
|
||
} else {
|
||
html += `<div class="success-box">✅ 执行成功 (影响 ${result ?? 0} 行)</div>`;
|
||
}
|
||
html += '<div style="height:12px"></div>';
|
||
}
|
||
resultArea.innerHTML = html;
|
||
updateRowCount(allResults.reduce((sum, r) => sum + (Array.isArray(r.result) ? r.result.length : 0), 0));
|
||
}
|
||
|
||
function renderTable(rows) {
|
||
if (!rows || rows.length === 0) {
|
||
resultArea.innerHTML = '<div class="success-box">✅ 查询返回 0 行</div>';
|
||
updateRowCount(0);
|
||
return;
|
||
}
|
||
resultArea.innerHTML = buildTableHTML(rows);
|
||
updateRowCount(rows.length);
|
||
}
|
||
|
||
function buildTableHTML(rows) {
|
||
const columns = Object.keys(rows[0]);
|
||
let html = '<table class="result-table"><thead><tr>';
|
||
for (const col of columns) {
|
||
html += `<th>${escapeHTML(col)}</th>`;
|
||
}
|
||
html += '</tr></thead><tbody>';
|
||
for (const row of rows) {
|
||
html += '<tr>';
|
||
for (const col of columns) {
|
||
const val = row[col];
|
||
html += `<td>${formatCell(val)}</td>`;
|
||
}
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody></table>';
|
||
return html;
|
||
}
|
||
|
||
function formatCell(val) {
|
||
if (val === null) return '<span style="color:#64748b;font-style:italic">NULL</span>';
|
||
if (val === undefined) return '<span style="color:#64748b">-</span>';
|
||
if (typeof val === 'boolean') return `<span style="color:#fbbf24">${val}</span>`;
|
||
if (typeof val === 'number') return `<span style="color:#fbbf24">${val}</span>`;
|
||
return escapeHTML(String(val));
|
||
}
|
||
|
||
function renderError(msg) {
|
||
resultArea.innerHTML = `<div class="error-box">❌ ${escapeHTML(msg)}</div>`;
|
||
updateRowCount(0);
|
||
}
|
||
|
||
function renderSuccess(msg) {
|
||
resultArea.innerHTML = `<div class="success-box">${msg}</div>`;
|
||
}
|
||
|
||
function renderInfo(msg) {
|
||
resultArea.innerHTML = `<div class="success-box" style="color:#22c55e;border-color:#22c55e;">${msg}</div>`;
|
||
}
|
||
|
||
function clearResults() {
|
||
resultArea.innerHTML = `<div class="empty-state"><div class="icon">⚡</div><div>结果已清空</div></div>`;
|
||
updateRowCount(0);
|
||
}
|
||
|
||
function updateRowCount(n) { rowCount.textContent = `${n} 行`; }
|
||
function escapeHTML(str) { return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
||
|
||
// Presets
|
||
const presets = {
|
||
all: `-- 查看所有数据
|
||
SELECT * FROM users;
|
||
|
||
SELECT * FROM orders;
|
||
|
||
SELECT * FROM products;`,
|
||
join: `-- INNER JOIN:用户 + 订单
|
||
SELECT u.name, o.product, o.amount, o.quantity
|
||
FROM users u
|
||
INNER JOIN orders o ON u.id = o.user_id
|
||
ORDER BY o.amount DESC;
|
||
|
||
-- LEFT JOIN:包含没有订单的用户
|
||
SELECT u.name, o.product, o.amount
|
||
FROM users u
|
||
LEFT JOIN orders o ON u.id = o.user_id;`,
|
||
agg: `-- 聚合统计:每个用户的消费
|
||
SELECT u.name, COUNT(*) as orders, SUM(o.amount) as total_spent
|
||
FROM users u
|
||
INNER JOIN orders o ON u.id = o.user_id
|
||
GROUP BY u.name
|
||
ORDER BY total_spent DESC;
|
||
|
||
-- 全局聚合
|
||
SELECT COUNT(*) as total_users, AVG(age) as avg_age,
|
||
MIN(age) as min_age, MAX(age) as max_age
|
||
FROM users;
|
||
|
||
-- 分类统计
|
||
SELECT category, COUNT(*) as product_count, AVG(price) as avg_price
|
||
FROM products
|
||
GROUP BY category;`,
|
||
ddl: `-- 查看所有表
|
||
-- (使用 db.getTableNames() 替代)
|
||
|
||
-- 创建新表
|
||
CREATE TABLE tasks (
|
||
id STRING PRIMARY KEY,
|
||
title STRING NOT NULL,
|
||
done BOOLEAN DEFAULT false,
|
||
priority NUMBER DEFAULT 0
|
||
);
|
||
|
||
-- 插入数据
|
||
INSERT INTO tasks (id, title, done, priority) VALUES ('t1', 'Learn SQLark', false, 1);
|
||
INSERT INTO tasks VALUES ('t2', 'Write docs', true, 2);
|
||
|
||
-- 查询
|
||
SELECT * FROM tasks ORDER BY priority DESC;
|
||
|
||
-- 删除表
|
||
DROP TABLE tasks;`,
|
||
crud: `-- 增
|
||
INSERT INTO users VALUES ('9', 'Frank', 'frank@demo.com', 40);
|
||
|
||
-- 查
|
||
SELECT * FROM users WHERE name = 'Frank';
|
||
|
||
-- 改
|
||
UPDATE users SET age = 41, email = 'frank2@demo.com' WHERE id = '9';
|
||
|
||
-- 查(确认)
|
||
SELECT * FROM users WHERE id = '9';
|
||
|
||
-- 删
|
||
DELETE FROM users WHERE id = '9';
|
||
|
||
-- 确认
|
||
SELECT COUNT(*) as user_count FROM users;`,
|
||
subquery: `-- IN 子查询:有订单的用户
|
||
SELECT name, email FROM users
|
||
WHERE id IN (SELECT user_id FROM orders);
|
||
|
||
-- NOT IN 子查询:没有订单的用户
|
||
SELECT name, email FROM users
|
||
WHERE id NOT IN (SELECT user_id FROM orders);
|
||
|
||
-- 标量子查询:消费最高的用户
|
||
SELECT name, age FROM users
|
||
WHERE id IN (
|
||
SELECT user_id FROM orders
|
||
WHERE amount = (SELECT MAX(amount) FROM orders)
|
||
);`,
|
||
tx: `-- 事务演示:使用 db.transaction() API
|
||
-- 查询用户 Alice
|
||
SELECT * FROM users WHERE id = '1';
|
||
|
||
-- 为 Alice 插入新订单
|
||
INSERT INTO orders VALUES ('o99', '1', 'Headphones', 199, 1);
|
||
|
||
-- 查看 Alice 的订单汇总
|
||
SELECT u.name, COUNT(*) as order_count, SUM(o.amount) as total
|
||
FROM users u INNER JOIN orders o ON u.id = o.user_id
|
||
WHERE u.id = '1'
|
||
GROUP BY u.name;
|
||
|
||
-- 生产环境用 db.transaction() 包裹保证原子性`,
|
||
scalar: `-- 聚合无 GROUP BY 返回单行
|
||
SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;
|
||
|
||
-- 年龄大于平均值的用户
|
||
SELECT name, age FROM users
|
||
WHERE age > (SELECT AVG(age) FROM users);
|
||
|
||
-- 消费超过平均值的用户
|
||
SELECT u.name, SUM(o.amount) as spent
|
||
FROM users u INNER JOIN orders o ON u.id = o.user_id
|
||
GROUP BY u.name
|
||
HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders);`,
|
||
cascade: `-- 外键级联:重建 orders 表带 CASCADE
|
||
DROP TABLE IF EXISTS orders;
|
||
|
||
CREATE TABLE orders (
|
||
id STRING PRIMARY KEY,
|
||
user_id STRING REFERENCES users(id) ON DELETE CASCADE,
|
||
product STRING,
|
||
amount NUMBER,
|
||
quantity NUMBER DEFAULT 1
|
||
);
|
||
|
||
-- 重新插入数据
|
||
INSERT INTO orders VALUES ('o1', '1', 'Laptop', 1200, 1);
|
||
INSERT INTO orders VALUES ('o2', '1', 'Mouse', 50, 2);
|
||
INSERT INTO orders VALUES ('o3', '2', 'Keyboard', 150, 1);
|
||
|
||
-- 查看关联数据
|
||
SELECT u.name, o.product, o.amount
|
||
FROM users u INNER JOIN orders o ON u.id = o.user_id;
|
||
|
||
-- 删除 Alice (id=1) 订单自动级联删除
|
||
DELETE FROM users WHERE id = '1';
|
||
|
||
-- 确认级联结果
|
||
SELECT u.name, o.product, o.amount
|
||
FROM users u LEFT JOIN orders o ON u.id = o.user_id;`,
|
||
adv: `-- BETWEEN 范围查询
|
||
SELECT * FROM users
|
||
WHERE age BETWEEN 20 AND 30 AND name != 'Bob'
|
||
ORDER BY age DESC;
|
||
|
||
-- DISTINCT 去重
|
||
SELECT DISTINCT age FROM users ORDER BY age;
|
||
|
||
-- 多表 JOIN + 过滤 + 排序 + 分页
|
||
SELECT u.name, o.product, o.amount
|
||
FROM users u
|
||
INNER JOIN orders o ON u.id = o.user_id
|
||
WHERE o.amount >= 50
|
||
ORDER BY o.amount DESC
|
||
LIMIT 5 OFFSET 0;
|
||
|
||
-- NOT LIKE 模糊排除
|
||
SELECT * FROM users
|
||
WHERE name NOT LIKE 'A%' AND age > 20;`,
|
||
alter: `-- 🏗 ALTER TABLE 动态修改表结构 (v0.2.5)
|
||
|
||
-- 添加新列
|
||
ALTER TABLE users ADD COLUMN phone STRING;
|
||
|
||
-- 插入数据(含新列)
|
||
INSERT INTO users (id, name, age, phone) VALUES ('6', 'Frank', 33, '123-4567');
|
||
|
||
-- 查询确认
|
||
SELECT * FROM users WHERE id = '6';
|
||
|
||
-- 删除列
|
||
ALTER TABLE users DROP COLUMN phone;
|
||
|
||
-- 查询确认(phone 列已删除)
|
||
SELECT * FROM users WHERE id = '6';`,
|
||
truncate: `-- 🗑 TRUNCATE TABLE 快速清空 (v0.2.5)
|
||
|
||
-- 创建临时表并插入数据
|
||
CREATE TABLE IF NOT EXISTS temp_logs (
|
||
id STRING PRIMARY KEY,
|
||
message STRING NOT NULL
|
||
);
|
||
INSERT INTO temp_logs VALUES ('1', 'log A');
|
||
INSERT INTO temp_logs VALUES ('2', 'log B');
|
||
INSERT INTO temp_logs VALUES ('3', 'log C');
|
||
|
||
-- 确认数据
|
||
SELECT COUNT(*) as total FROM temp_logs;
|
||
|
||
-- TRUNCATE 快速清空
|
||
TRUNCATE TABLE temp_logs;
|
||
|
||
-- 确认清空
|
||
SELECT COUNT(*) as total FROM temp_logs;
|
||
|
||
-- 清理
|
||
DROP TABLE temp_logs;`,
|
||
aria: `-- 🌲 AriaEngine 演示 (v0.7.4)
|
||
-- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine
|
||
-- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter)
|
||
-- 基础 CRUD 与 Memory 引擎完全兼容
|
||
|
||
-- 当前引擎确认
|
||
-- (Aria 引擎实例,数据经 WAL + SSTable 持久化到 OPFS)
|
||
CREATE TABLE IF NOT EXISTS tasks (
|
||
id STRING PRIMARY KEY,
|
||
title STRING NOT NULL,
|
||
done BOOLEAN DEFAULT false
|
||
);
|
||
|
||
-- 插入数据
|
||
INSERT INTO tasks VALUES ('t1', 'Implement AriaEngine', false);
|
||
INSERT INTO tasks VALUES ('t2', 'Write tests', false);
|
||
INSERT INTO tasks VALUES ('t3', 'Update docs', true);
|
||
|
||
-- 查询
|
||
SELECT * FROM tasks ORDER BY title;
|
||
|
||
-- 聚合统计
|
||
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: 分片文件 + 标准 CRC32 + 空洞检测 + 崩溃恢复(残缺 SSTable 自动跳过)
|
||
-- • MVCC: 版本链 + 快照隔离
|
||
-- • 页面化物理存储: SSTable 4KB 页面 + BufferPool LRU 缓存 (v0.5.0)
|
||
-- • 完整性: 整文件 CRC-32 校验(数据腐坏自动清理自愈)
|
||
-- • Bloom Filter: FNV-1a + Murmur 双哈希
|
||
-- • 二级索引: 每列独立 LSM + 跨重启自动恢复
|
||
-- • 外键级联: ON DELETE / ON UPDATE — CASCADE / SET NULL / RESTRICT
|
||
-- • 自愈: db.repair() 无需删库重建(含孤儿页面/残留清理)
|
||
-- • 维护语句: EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT (v0.5.1)
|
||
|
||
-- 生产环境 API(与演示页右上角切换等价)
|
||
-- const db = await MetonaSqlark.create({
|
||
-- name: 'my-app', mode: 'aria'
|
||
-- });`,
|
||
maint: `-- 🛠 维护语句 (v0.5.1)
|
||
-- EXPLAIN: 输出查询计划(任何引擎可用)
|
||
EXPLAIN SELECT * FROM users WHERE age > 18;
|
||
|
||
-- ANALYZE: 收集表统计信息(仅 Aria 引擎)
|
||
ANALYZE TABLE users;
|
||
|
||
-- REINDEX: 重建二级索引(仅 Aria 引擎)
|
||
REINDEX TABLE users;
|
||
|
||
-- VACUUM: 压缩 LSM + 清理碎片(仅 Aria 引擎)
|
||
VACUUM;
|
||
|
||
-- SAVEPOINT: 嵌套事务保存点(仅 Aria 引擎)
|
||
BEGIN;
|
||
UPDATE users SET age = age + 1 WHERE id = '1';
|
||
SAVEPOINT sp1;
|
||
UPDATE users SET age = 0 WHERE id = '1';
|
||
ROLLBACK TO SAVEPOINT sp1;
|
||
RELEASE SAVEPOINT sp1;
|
||
COMMIT;
|
||
SELECT id, age FROM users WHERE id = '1';`,
|
||
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);
|
||
|
||
-- 索引查找(v0.4.2: JOIN 主表 WHERE 条件下推 + 二级索引跨重启恢复)
|
||
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';
|
||
|
||
-- DROP 不存在的索引会报错(INDEX_NOT_FOUND)
|
||
-- DROP INDEX idx_nonexist ON orders (user_id);`,
|
||
onupdate: `-- 🔄 ON UPDATE 外键级联 (v0.4.2)
|
||
-- 更新父表主键 → 子表外键自动级联(CASCADE / SET NULL / RESTRICT)
|
||
|
||
-- 建带 onUpdate 外键的表
|
||
DROP TABLE IF EXISTS accounts;
|
||
DROP TABLE IF EXISTS audit_log;
|
||
CREATE TABLE accounts (id STRING PRIMARY KEY, owner STRING);
|
||
CREATE TABLE audit_log (
|
||
id STRING PRIMARY KEY,
|
||
account_id STRING REFERENCES accounts.id ON UPDATE CASCADE
|
||
);
|
||
|
||
-- 种子数据
|
||
INSERT INTO accounts VALUES ('a1', 'Alice');
|
||
INSERT INTO audit_log VALUES ('l1', 'a1');
|
||
INSERT INTO audit_log VALUES ('l2', 'a1');
|
||
|
||
-- 更新父表主键 a1 → a2
|
||
UPDATE accounts SET id = 'a2' WHERE id = 'a1';
|
||
|
||
-- 子表外键已级联为 a2
|
||
SELECT * FROM audit_log ORDER BY id;
|
||
|
||
-- 再按新主键反查
|
||
SELECT * FROM accounts WHERE id = 'a2';`,
|
||
repair: `-- 🛡 崩溃恢复自愈 (v0.4.2)
|
||
-- db.repair(): 校验并清理损坏 SSTable / 重建二级索引 / 截断 WAL
|
||
-- db.clearAll(): 清空全部数据与表结构(保留库本身)
|
||
-- 打开数据库时自动跳过残缺 SSTable,无需删库重建
|
||
|
||
-- 数据写入
|
||
CREATE TABLE IF NOT EXISTS notes (id STRING PRIMARY KEY, body STRING);
|
||
INSERT INTO notes VALUES ('n1', 'Hello');
|
||
INSERT INTO notes VALUES ('n2', 'World');
|
||
|
||
-- 模拟数据(正常数据)
|
||
SELECT * FROM notes ORDER BY id;
|
||
|
||
-- API 自愈(控制台执行):
|
||
-- await db.repair(); → 校验+清理损坏文件,重建索引
|
||
-- await db.clearAll(); → 清空全部表与数据
|
||
|
||
-- 迁移版本持久化(重启后不重跑已执行迁移)
|
||
-- db.addMigration(1, async () => { ... });
|
||
-- await db.migrateTo(1);`,
|
||
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;`,
|
||
v040: `-- 🚰 v0.4.0 新特性
|
||
|
||
-- 派生表:FROM (SELECT ...) 子查询作为行源
|
||
SELECT category, total FROM (
|
||
SELECT category, SUM(price) AS total FROM products GROUP BY category
|
||
) AS t WHERE total > 100 ORDER BY total DESC;
|
||
|
||
-- COUNT(DISTINCT col):去重计数
|
||
SELECT COUNT(DISTINCT category) AS categories FROM products;
|
||
|
||
-- NULLS FIRST / LAST:NULL 排序位置控制
|
||
SELECT name FROM users ORDER BY age ASC NULLS FIRST LIMIT 3;
|
||
|
||
-- 普通列别名 + ORDER BY 别名
|
||
SELECT name AS n FROM users ORDER BY n DESC;
|
||
|
||
-- SQL 标准 '' 字符串转义
|
||
SELECT 'it''s a test' AS escaped;
|
||
|
||
-- 哈希连接(等值 ON 走 O(N+M);多列等值 ON 同样支持)
|
||
SELECT u.name, o.product FROM users u
|
||
INNER JOIN orders o ON o.user_id = u.id;`,
|
||
};
|
||
|
||
// v0.4.1: 点击预设前自动重置数据(前序预设可能修改/删除种子数据,重置保证每个预设可预期演示)
|
||
async function loadPreset(name) {
|
||
if (presets[name]) {
|
||
sqlInput.value = presets[name];
|
||
await initDB();
|
||
runQuery();
|
||
}
|
||
}
|
||
|
||
// Keyboard shortcut
|
||
document.addEventListener('keydown', e => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
e.preventDefault();
|
||
runQuery();
|
||
}
|
||
});
|
||
|
||
// Boot
|
||
document.getElementById('btn-memory').style.opacity = '1';
|
||
document.getElementById('btn-aria').style.opacity = '0.6';
|
||
document.getElementById('engine-status').textContent = '⚡ Memory';
|
||
initDB().then(() => {
|
||
console.log('✅ MetonaSqlark v0.4.2 demo ready');
|
||
setTimeout(runQuery, 300);
|
||
}).catch(err => {
|
||
renderError('初始化失败: ' + err.message);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|