Files
MetonaSqlark/site/index.html
T
thzxx 0b44620721 fix(v0.8.0): 全量回归审查 —— 1 处 P0 数据丢失 + 4 处 P1 + 9 处 P2 根因修复
方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 vs 实现 / 公共 API 契约 /
测试质量),每条结论要求可复现证据;逐条复核 + 探针确认 + 变异验证(40 项全部
被对应用例拦住)。

P0:事务活跃期间 repair()/close()/周期 checkpoint 推进 WAL 水位 → 已 COMMIT 的
事务整批消失且恢复报告"干净"。根因 hasPendingFlushData()/computeDurableLsn()
不看 txnSnapshot;守卫此前只在 CheckpointManager 两个回调里。修复:守卫下沉到
computeDurableLsn() 与 advanceWalCheckpoint() 入口(唯一实现)。

P1:
- WAL 前缀缺失丢弃整段活分片(回退上一代 manifest 时 kept 为空)→ 前缀缺失单独
  记录,后缀照常重放;仅 fromLsn === 0 时才算真异常
- 孤儿回收门槛只看引擎层 dataLossSuspected,漏掉 LSM 层被丢的 SSTable →
  统一 describeRecoveryDamage() 聚合判定(损坏时绝不删"引用不到"的文件)
- vacuum() 逐层压缩绕过维护链 → vacuumLevels() 每层作为维护链任务执行
- reclaimRetiredNow() 无视在途读者(读者把"已退休"读成"文件损坏")→ 有读者时
  退化为延迟回收

P2:WAL 记录级 CRC 损坏不计数不上报;旧格式表结构记录形状损坏静默当空库;
bloomFilterBitsPerKey 配置被接受却完全不生效(构建器写死默认值,实现缺陷);
幽灵 meta;介质读故障等于文件损坏的语义无用例;manifest 回读校验两条守卫无用例;
文件名≠载荷世代判定无用例;pageIdWatermark 单调性无用例;分片号两条真实不变量
无用例。

覆盖率口径(第二处漏洞):interface.ts 混着三个运行时函数(cloneRow 等)却被
描述为"纯类型、不纳入统计" → 实现搬到 src/engine/row_clone.ts;搬完门禁真的
失败(functions 93.84% < 94%),补测退化路径后通过。

测试质量:3 条空壳用例改值级断言;1 条"全损坏"用例实际只走缓存 → 拆成两条真
用例;5 秒墙钟 race 改门控 + 失败上限;setTimeout 改 whenIdle();<= 收紧为 <。

变异脚本加固:正控(干净基线必须全绿)、编译失败/0 用例单独归类、300s 超时、
逐字节 sha256 恢复校验、O_EXCL 进程锁、锚点唯一性;变异 22 → 40 项。

文档两轮订正(16 + 11 条不成立宣称):MVCC 快照隔离、backup 一致性快照、
"空洞检测截断"、体积(251,109 B / gzip 63,145 B)、测试与覆盖率数字、
"5 种存储引擎"、Tree-shakable、错误码表补 16 个码、恢复报告字段、已知限制
(回退单向 / 多实例依赖 Web Locks / manifest 体积 / 尾部 WAL 分片不可识别)。

验证:常规套件 92 套件 / 1980 用例全绿;覆盖率 90.59 / 82.59 / 94.14 / 93.50
(阈值 90/82/94/93);e2e 14/14(真实 Chromium + OPFS + CDP 崩溃);
重型套件 4 套件 / 27 用例;变异 40/40;lint + 两份 tsc 干净;dist 已重建。
2026-09-15 16:33:40 +08:00

495 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>◈ MetonaSqlark — 前端 TypeScript 数据库</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: #14141f;
--surface2: #1a1a2e;
--border: #2a2a45;
--primary: #6366f1;
--primary-glow: #818cf8;
--accent: #06b6d4;
--accent2: #ec4899;
--text: #e2e8f0;
--text2: #94a3b8;
--gradient: linear-gradient(135deg, #6366f1 0%, #06b6d4 50%, #ec4899 100%);
--gradient2: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #1e1b4b 100%);
--radius: 16px;
--shadow: 0 0 60px rgba(99,102,241,0.15);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
overflow-x: hidden;
}
/* --- Particles background --- */
.bg-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:0; opacity:0.3; pointer-events:none; }
/* --- Layout --- */
.container { max-width:1200px; margin:0 auto; padding:0 24px; position:relative; z-index:1; }
/* --- Header --- */
header {
position:fixed; top:0; left:0; right:0; z-index:100;
backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
background:rgba(10,10,15,0.85); border-bottom:1px solid var(--border);
padding:16px 0;
}
header .container { display:flex; align-items:center; justify-content:space-between; }
.logo { font-size:1.4rem; font-weight:800; letter-spacing:-0.5px; display:flex; align-items:center; gap:10px; }
.logo .icon {
width:36px; height:36px; border-radius:10px;
background:var(--gradient); display:flex; align-items:center; justify-content:center;
font-size:1.2rem; font-weight:900; color:#fff;
}
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:32px; }
nav a { color:var(--text2); text-decoration:none; font-weight:500; font-size:0.95rem; transition:.2s; position:relative; }
nav a:hover { color:var(--text); }
nav a::after { content:''; position:absolute; bottom:-4px; left:0; width:0; height:2px; background:var(--gradient); transition:.3s; border-radius:1px; }
nav a:hover::after { width:100%; }
.btn {
display:inline-flex; align-items:center; gap:8px; padding:10px 24px; border-radius:10px;
font-weight:600; font-size:0.95rem; cursor:pointer; border:none; transition:.3s; text-decoration:none;
}
.btn-primary { background:var(--gradient); color:#fff; box-shadow:0 4px 20px rgba(99,102,241,0.4); }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 8px 30px rgba(99,102,241,0.6); }
.btn-outline { background:transparent; border:2px solid var(--border); color:var(--text); }
.btn-outline:hover { border-color:var(--primary); background:rgba(99,102,241,0.08); }
/* --- Hero --- */
.hero {
padding:160px 0 80px; text-align:center; position:relative;
background:radial-gradient(ellipse 80% 60% at 50% -10%, rgba(99,102,241,0.15), transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 80%, rgba(6,182,212,0.08), transparent 60%);
}
.hero h1 { font-size:clamp(2.4rem, 6vw, 4rem); font-weight:900; letter-spacing:-1.5px; line-height:1.1; margin-bottom:24px; }
.hero h1 .gradient-text { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.hero p { font-size:1.2rem; color:var(--text2); max-width:620px; margin:0 auto 40px; }
.hero .actions { display:flex; gap:16px; justify-content:center; flex-wrap:wrap; }
.badge {
display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:999px;
background:var(--surface2); border:1px solid var(--border); font-size:0.85rem; color:var(--text2);
}
.badge .dot { width:7px; height:7px; border-radius:50%; background:#22c55e; animation:pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
/* --- Section --- */
section { padding:100px 0; }
.section-title { text-align:center; margin-bottom:60px; }
.section-title h2 { font-size:2.2rem; font-weight:800; letter-spacing:-0.5px; margin-bottom:12px; }
.section-title h2 span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.section-title p { color:var(--text2); font-size:1.1rem; }
/* --- Feature Grid --- */
.feature-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:24px; }
.feature-card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
padding:32px; transition:.3s; position:relative; overflow:hidden;
}
.feature-card:hover { border-color:var(--primary); transform:translateY(-4px); box-shadow:var(--shadow); }
.feature-card::before {
content:''; position:absolute; top:0; left:0; right:0; height:3px;
background:var(--gradient); opacity:0; transition:.3s;
}
.feature-card:hover::before { opacity:1; }
.feature-card .icon { font-size:2rem; margin-bottom:16px; }
.feature-card h3 { font-size:1.2rem; margin-bottom:8px; }
.feature-card p { color:var(--text2); font-size:0.95rem; }
/* --- Code Block --- */
.code-block {
background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius);
padding:28px; font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.88rem;
overflow-x:auto; position:relative; line-height:1.7;
}
.code-block .keyword { color:#c084fc; }
.code-block .string { color:#34d399; }
.code-block .func { color:#60a5fa; }
.code-block .comment { color:#64748b; }
.code-block .number { color:#fbbf24; }
.code-block .type { color:#f472b6; }
/* --- Stats --- */
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:24px; text-align:center; }
.stat-card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:32px;
}
.stat-card .num { font-size:2.8rem; font-weight:900; background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.stat-card .label { color:var(--text2); font-size:0.95rem; margin-top:4px; }
/* --- Footer --- */
footer { border-top:1px solid var(--border); padding:40px 0; text-align:center; color:var(--text2); }
footer a { color:var(--primary); text-decoration:none; }
/* --- Responsive --- */
@media(max-width:768px){
nav { display:none; }
.hero { padding:120px 0 60px; }
section { padding:60px 0; }
}
</style>
</head>
<body>
<canvas class="bg-canvas" id="bg"></canvas>
<header>
<div class="container">
<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">基准</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a>
</nav>
<a href="demo.html" class="btn btn-primary">立即体验</a>
</div>
</header>
<!-- Hero -->
<section class="hero">
<div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.8.0 根治性迭代 — 1980 测试 92 套件 · 语句/分支/函数/行覆盖率 90.59% / 82.59% / 94.14% / 93.50% · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,4 种存储模式 + 3 种存储后端,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions">
<a href="demo.html" class="btn btn-primary" style="font-size:1.05rem;padding:14px 32px;">▶ 在线演示</a>
<a href="docs.html" class="btn btn-outline" style="font-size:1.05rem;padding:14px 32px;">📖 API 文档</a>
</div>
</div>
</section>
<!-- Install -->
<section style="padding:20px 0 60px;">
<div class="container">
<div class="section-title">
<h2>📦 <span>安装</span>引入</h2>
<p>多种方式,按需选择</p>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="icon">📦</div>
<h3>npm (Gitea Registry)</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="comment"># 配置 registry</span>
npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
<span class="comment"># 安装</span>
npm install @metona-team/metona-sqlark
</div>
</div>
<div class="feature-card">
<div class="icon">🌐</div>
<h3>CDN / Browser UMD</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="comment">&lt;!-- 直接引用 dist 产物 --&gt;</span>
&lt;<span class="keyword">script</span> src=<span class="string">"metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;<span class="keyword">script</span>&gt;
<span class="keyword">const</span> db = <span class="keyword">await</span> window.<span class="func">MetonaSqlark</span>.create({...});
<span class="comment">// 或 window.MeSqlark(别名)</span>
&lt;/script&gt;
</div>
</div>
<div class="feature-card">
<div class="icon"></div>
<h3>ESM / TypeScript</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="keyword">import</span> { MetonaSqlark, MeSqlark }
<span class="keyword">from</span> <span class="string">'@metona-team/metona-sqlark'</span>;
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark</span>.create({
<span class="string">name</span>: <span class="string">'my-app'</span>,
<span class="string">mode</span>: <span class="string">'hybrid'</span>,
});
</div>
</div>
<div class="feature-card">
<div class="icon">🔧</div>
<h3>CommonJS</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="keyword">const</span> { MetonaSqlark } =
<span class="func">require</span>(<span class="string">'@metona-team/metona-sqlark'</span>);
(<span class="keyword">async</span> () => {
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark</span>.create({...});
})();
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section>
<div class="container">
<div class="section-title">
<h2>为什么选择 <span>MetonaSqlark</span></h2>
<p>专为前端打造的数据库引擎</p>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="icon">🧠</div>
<h3>4 种存储模式 + 3 种后端</h3>
<p>模式:Memory / Disk / Hybrid / <strong>AriaEngine</strong>;后端:OPFS / KVStore / Memory。Aria 是自研 LSM-Tree 页面式引擎,支持 WAL 同步崩溃恢复与事务回滚(版本链仅作事务内 undo,事务串行 —— 非 MVCC 快照隔离)。</p>
</div>
<div class="feature-card">
<div class="icon"></div>
<h3>完整 SQL 解析器</h3>
<p>手写递归下降 SQL 解析器。SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / DISTINCT / 子查询 / ALTER TABLE / TRUNCATE TABLE。</p>
</div>
<div class="feature-card">
<div class="icon">🌲</div>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.8.0</span></h3>
<p>自研 LSM-Tree 存储引擎:SSTable 4KB 页面化物理存储(BufferPool LRU)、WAL 分片文件原子写入、<strong>__aria_manifest 单一提交点</strong>(数据落盘 → 元数据原子提交 → 才截断 WAL)、全库 AES-GCM 透明加密、标准 CRC-32 完整性校验、MVCC 快照回滚、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联、崩溃恢复自愈。</p>
</div>
<div class="feature-card">
<div class="icon">🔒</div>
<h3>事务回滚 + MVCC</h3>
<p>beginTransaction / commitTransaction / rollbackTransaction 三件套。失败自动回滚,Memory 快照 + KVStore 原子日志 + Aria MVCC 版本链。</p>
</div>
<div class="feature-card">
<div class="icon">🔍</div>
<h3>子查询 + ALTER TABLE</h3>
<p>IN (SELECT ...) + 标量子查询。ALTER TABLE ADD/DROP COLUMN 动态修改表结构,TRUNCATE TABLE 快速清空。</p>
</div>
<div class="feature-card">
<div class="icon">🔗</div>
<h3>外键级联 + SQL安全</h3>
<p>references + ON DELETE / ON UPDATE CASCADE · SET NULL · RESTRICT(更新主键级联更新子表)。React/Vue hooks 表名合法性校验,防 SQL 注入。</p>
</div>
<div class="feature-card">
<div class="icon">🛡</div>
<h3>崩溃恢复自愈</h3>
<p>异常退出后无需删库重建:整文件 CRC-32 校验 + 打开自动跳过损坏 SSTable,并按 WAL 水位重放已提交事务;<code>getRecoveryReport()</code> 如实给出被丢弃的 SSTable、WAL 空洞与是否怀疑丢数据。介质<strong>读故障</strong>与"文件不存在"分开处理(前者抛 ARIA_SSTABLE_READ_FAILED,绝不误删元数据);元数据整体损坏时显式拒绝打开(ARIA_MANIFEST_CORRUPT),而不是当成空库。db.repair() 只在 manifest 健康时回收孤儿页面并重建索引。</p>
</div>
<div class="feature-card">
<div class="icon">🏊</div>
<h3>连接池 + 性能优化</h3>
<p>MetonaSqlark.connect() 单例复用。SSTableReader 二分查找统一、KVStore 内存热路径 O(1) 读、crypto 实例化。</p>
</div>
<div class="feature-card">
<div class="icon">🧩</div>
<h3>插件系统</h3>
<p>14 种生命周期钩子全部真实触发(Table API 与 SQL 双路径):beforeCreateTable / afterInsert / beforeQuery / afterTransaction……按优先级注册。</p>
</div>
<div class="feature-card">
<div class="icon">📦</div>
<h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。UMD/ESM/CJS 多格式输出,min 产物实测 251,109 字节 / gzip 63,145 字节。</p>
</div>
<div class="feature-card">
<div class="icon">📊</div>
<h3>聚合 & 分组</h3>
<p>COUNT / SUM / AVG / MIN / MAX 五大聚合函数,GROUP BY + HAVING 子句,DISTINCT 去重。</p>
</div>
<div class="feature-card">
<div class="icon">🔐</div>
<h3>全库加密 + 多标签页锁</h3>
<p>encryption.password 一键启用 AES-256-GCM 全库透明加密(密码验证 + 篡改检测)。Web Locks 独占锁:第二个标签页打开同一库抛 ARIA_LOCKED。</p>
</div>
<div class="feature-card">
<div class="icon">🛠</div>
<h3>维护语句(SQL</h3>
<p>EXPLAIN 查询计划 · ANALYZE 表统计 · REINDEX 重建索引 · VACUUM 压缩 · SAVEPOINT 嵌套事务保存点 — 全部原生 SQL 语法。</p>
</div>
<div class="feature-card">
<div class="icon">🔄</div>
<h3>数据迁移</h3>
<p>内置版本迁移系统,addMigration + migrateTo API。导入导出支持单表/全库 JSON 序列化,db.backup() 全库导出(逐表读取)。</p>
</div>
<div class="feature-card">
<div class="icon">📡</div>
<h3>发布订阅 + 框架集成</h3>
<p>subscribe() 订阅表变更。React useQuery / Vue useSqlarkQuery 开箱即用。</p>
</div>
<div class="feature-card">
<div class="icon">🔗</div>
<h3>Query Builder</h3>
<p>链式 API + TypeScript 泛型。.select().where().innerJoin().orderBy().limit() — 类型安全,IDE 友好。</p>
</div>
<div class="feature-card">
<div class="icon">🚰</div>
<h3>流式查询 + 派生表</h3>
<p>queryStream 逐行回调真惰性扫描(v0.7.4:Aria 引擎迭代器化,limit 提前终止,大表内存 O(1))。FROM (SELECT ...) 派生表、多列哈希连接、COUNT(DISTINCT)、NULLS FIRST/LAST。</p>
</div>
</div>
</div>
</section>
<!-- Architecture -->
<section>
<div class="container">
<div class="section-title">
<h2>🏗 <span>架构</span>设计</h2>
<p>清晰的分层结构,各模块职责单一</p>
</div>
<div class="code-block" style="text-align:center;font-size:0.82rem;line-height:2;background:transparent;border:none;">
<pre style="color:var(--text2);">
┌──────────────────────────────────────────────────────────┐
<span style="color:#f472b6;">MetonaSqlark / MeSqlark</span>
│ create() · query() · table() · tx() │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
<span style="color:#c084fc;">SQL Parser</span><span style="color:#c084fc;">QueryBuilder</span><span style="color:#c084fc;">Transaction</span><span style="color:#c084fc;">Plugin</span><span style="color:#c084fc;">Migration</span>
│ Lexer → │ .select() │ Manager │ Manager │ System │
│ Parser │ .where() │ │ 14 hooks │ │
├──────────┴──────────┴──────────┴──────────┴──────────────┤
<span style="color:#60a5fa;">QueryExecutor</span> (AST → Results) │
│ JOIN · GROUP BY · HAVING · DISTINCT · Subquery │
├──────────────────────────────────────────────────────────┤
<span style="color:#34d399;">IStorageEngine</span> Interface │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
<span style="color:#fbbf24;">Memory </span><span style="color:#fbbf24;">KVStore </span><span style="color:#fbbf24;">OPFS </span><span style="color:#fbbf24;">Hybrid </span><span style="color:#ec4899;font-weight:bold;">AriaEngine</span>
│ Engine │ Engine │ Engine │ Engine │ LSM-Tree │
│ (Map) │ (自研KV) │ (OPFS) │ (W-Thru) │ WAL + MVCC │
└──────────┴──────────┴──────────┴──────────┴──────────────┘
</pre>
</div>
</div>
</section>
<section style="background:var(--gradient2);">
<div class="container">
<div class="section-title">
<h2>5 行<span>代码</span>开始</h2>
<p>SQL + Query Builder 双 API — JOIN · 子查询 · 聚合 · 事务回滚 · 外键级联 · 连接池 · AriaEngine · ALTER TABLE · TRUNCATE</p>
</div>
<div class="code-block">
<pre><span class="comment">// 创建数据库 — MeSqlark 是别名,完全等价</span>
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark.create</span>({ <span class="string">name</span>: <span class="string">'my-app'</span>, <span class="string">mode</span>: <span class="string">'hybrid'</span> });
<span class="comment">// 定义表结构 — 支持外键级联</span>
<span class="keyword">await</span> db.<span class="func">defineTable</span>(<span class="string">'users'</span>, {
<span class="string">id</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">primaryKey</span>: <span class="keyword">true</span> },
<span class="string">name</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">required</span>: <span class="keyword">true</span> },
<span class="string">age</span>: { <span class="string">type</span>: <span class="string">'number'</span>, <span class="string">default</span>: <span class="number">0</span> },
<span class="string">email</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">unique</span>: <span class="keyword">true</span>, <span class="string">index</span>: <span class="keyword">true</span> },
});
<span class="comment">// SQL 查询</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">"INSERT INTO users VALUES ('1', 'Alice', 30, 'alice@demo.com')"</span>);
<span class="keyword">const</span> rows = <span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">'SELECT * FROM users WHERE age > 18 ORDER BY name'</span>);
<span class="comment">// Query Builder — 类型安全,链式调用</span>
<span class="keyword">const</span> rows2 = <span class="keyword">await</span> db.<span class="func">table</span>(<span class="string">'users'</span>)
.<span class="func">select</span>([<span class="string">'name'</span>, <span class="string">'age'</span>]).<span class="func">where</span>({ <span class="string">age</span>: { <span class="string">$gt</span>: <span class="number">18</span> } })
.<span class="func">orderBy</span>(<span class="string">'name'</span>).<span class="func">limit</span>(<span class="number">10</span>).<span class="func">execute</span>();
<span class="comment">// JOIN 多表关联</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT u.name, o.product, o.amount
FROM users u INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100`</span>);
<span class="comment">// GROUP BY 聚合分析</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT dept, COUNT(*) as cnt, AVG(salary) as avg
FROM employees GROUP BY dept HAVING COUNT(*) > 1`</span>);
<span class="comment">// 事务 — 失败自动回滚</span>
<span class="keyword">await</span> db.<span class="func">transaction</span>(<span class="keyword">async</span> (trx) => {
<span class="keyword">await</span> trx.<span class="func">table</span>(<span class="string">'users'</span>).<span class="func">insert</span>({ id: <span class="string">'3'</span>, name: <span class="string">'Charlie'</span> });
<span class="keyword">await</span> trx.<span class="func">table</span>(<span class="string">'orders'</span>).<span class="func">insert</span>({ id: <span class="string">'o1'</span>, userId: <span class="string">'3'</span>, amount: <span class="number">99</span> });
<span class="comment">// 任何一步失败 → 自动回滚</span>
});
<span class="comment">// 子查询 — IN (SELECT ...)</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`</span>);
<span class="comment">// 连接池 — 单例复用,避免重复打开底层存储</span>
<span class="keyword">const</span> db1 = <span class="keyword">await</span> <span class="func">MetonaSqlark.connect</span>({ name: <span class="string">'my-app'</span> });
<span class="keyword">const</span> db2 = <span class="keyword">await</span> <span class="func">MetonaSqlark.connect</span>({ name: <span class="string">'my-app'</span> });
<span class="comment">// db1 === db2(复用已有实例)</span>
<span class="keyword">await</span> db2.<span class="func">disconnect</span>(); <span class="comment">// 引用计数 -1,归零时自动 close</span></pre>
</div>
</div>
</section>
<!-- Stats -->
<section>
<div class="container">
<div class="section-title">
<h2><span>数字</span>说话</h2>
<p>MetonaSqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">1980</div><div class="label">测试用例(+14 e2e</div></div>
<div class="stat-card"><div class="num">93.50%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">63KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">4+3</div><div class="label">存储模式+后端</div></div>
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">91</div><div class="label">测试套件</div></div>
</div>
</div>
</section>
<footer>
<div class="container">
<p>© 2026 <a href="https://git.metona.cn/MetonaTeam/MetonaSqlark">MetonaTeam</a>. MIT License. Built with TypeScript.</p>
</div>
</footer>
<script>
// Particle background
const canvas = document.getElementById('bg');
const ctx = canvas.getContext('2d');
let particles = [];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize(); window.addEventListener('resize', resize);
class Particle {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.5;
this.vy = (Math.random() - 0.5) * 0.5;
this.size = Math.random() * 2 + 0.5;
this.opacity = Math.random() * 0.4 + 0.1;
}
update() {
this.x += this.vx; this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(99,102,241,${this.opacity})`;
ctx.fill();
}
}
for (let i = 0; i < 80; i++) particles.push(new Particle());
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => { p.update(); p.draw(); });
// Draw connections
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(99,102,241,${0.06 * (1 - dist / 120)})`;
ctx.stroke();
}
}
}
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>