feat: site全面更新 + CONTRIBUTING/CHANGELOG完善,准备v0.1.13迭代
CI / test (18.x) (push) Successful in 9m55s
CI / test (20.x) (push) Successful in 9m50s
CI / test (22.x) (push) Successful in 9m52s
CI / test (24.x) (push) Successful in 9m48s

This commit is contained in:
thzxx
2026-07-26 16:13:34 +08:00
parent 7858cf3720
commit 7a53cfd530
5 changed files with 811 additions and 265 deletions
+60 -3
View File
@@ -1,8 +1,65 @@
# Changelog # Changelog
All notable changes to YOUR-PROJECT will be documented in this file. All notable changes to MetonaSqlark will be documented in this file.
## [0.0.1] - 2026-07-25 ## [0.1.12] - 2026-07-26
### Added
- `MeSqlark` 别名导出,与 `MetonaSqlark` 完全等价
- `metona-sqlark.cjs.js` CommonJS 构建产物
- 完善的 `where-matcher.ts` 消除 220+ 行重复代码
- `$col` 列引用支持 JOIN ON 条件
- `$and/$or` 嵌套支持(字段级 + 顶层)
- `$not` 逻辑非操作符
- `$like` 正则缓存加速
- `projectColumns` 列投影支持 `table.column` 格式
- `DISTINCT` 去重支持(O(n) 时间,列值拼接优化)
- 测试覆盖率提升至 93.46%(264 测试/15 套件)
### Changed
- `where-matcher` 统一 MemoryEngine / IndexedDBEngine / Executor 的 WHERE 逻辑
- JOIN ON 匹配支持 `$col` 语法
- LIKE 正则改为缓存式编译
- Executor DISTINCT 用列值拼接代替 JSON.stringify
- JOIN 嵌套循环连接优化:避免 ON 时对象扩散
### Fixed
- IndexedDBEngine `update/delete` 方法正确同步内存缓存
- OPFSEngine `getTableNames` 兼容 `entries()` 返回值
- SQL Parser 正确处理字符串转义字符
- SQL Parser 正确处理表别名(无 AS 关键字)
- SQL Parser 解析 `IS NULL` / `IS NOT NULL` / `NOT LIKE`
- `peekTokenIs` 方法正确暴露给语法分析
- 多处边界条件空值/假值处理
---
## [0.1.11] - 2026-07-25
### Added
- React 集成 hooks: `useQuery`, `useTable`, `useDatabase`
- Vue 集成 composables: `useSqlarkQuery`, `useSqlarkTable`, `useSqlarkDatabase`
- 发布订阅系统: `subscribe()`, `emit()`
- 数据迁移系统: `addMigration()`, `migrateTo()`
- 导入导出: `exportTable()`, `exportAll()`, `importTable()`
- 完整 SQL 解析器: `Lexer` + `Parser`(递归下降)
- SQL 支持: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
- JOIN 支持: INNER, LEFT, RIGHT, CROSS
- GROUP BY + HAVING + 聚合函数 (COUNT, SUM, AVG, MIN, MAX)
- DISTINCT, ORDER BY, LIMIT, OFFSET
- WHERE 条件: AND, OR, NOT, IN, LIKE, IS NULL
- Query Builder 链式 API: `select().where().orderBy().limit().execute()`
- 插件系统: 14 种生命周期钩子 + PluginManager
- Metadata 系统: ColumnDef 完整约束 (type/primaryKey/required/unique/index/default/references/maxLength/min/max)
### Changed
- 架构重构为分层设计:Engine → QueryExecutor → Table/QueryBuilder → SQL Parser → Public API
- AST 作为统一中间表示,SQL 和 QueryBuilder 行为完全一致
- 测试框架完善:core / edge / groupby / join / query-system / sql / table / engine / transaction / plugin / hybrid 全覆盖
---
## [0.0.1] - 2026-07-24
### Added ### Added
- 初始项目骨架 - 初始项目骨架
@@ -12,4 +69,4 @@ All notable changes to YOUR-PROJECT will be documented in this file.
- ESLint + @typescript-eslint - ESLint + @typescript-eslint
- Gitea Actions CI 工作流 - Gitea Actions CI 工作流
- 示例站点(index / demo / docs - 示例站点(index / demo / docs
- 构建脚本 build.sh / serve.sh - 构建脚本 build.sh
+109 -50
View File
@@ -1,4 +1,4 @@
# Contributing to MetonaEditor # Contributing to MetonaSqlark
Thanks for your interest in contributing! This document outlines the development workflow and conventions. Thanks for your interest in contributing! This document outlines the development workflow and conventions.
@@ -10,8 +10,8 @@ Thanks for your interest in contributing! This document outlines the development
## Setup ## Setup
```bash ```bash
git clone https://git.metona.cn/MetonaTeam/MetonaEditor.git git clone https://git.metona.cn/MetonaTeam/MetonaSqlark.git
cd MetonaEditor cd MetonaSqlark
npm install npm install
``` ```
@@ -39,34 +39,33 @@ npm run format
``` ```
src/ src/
├── index.ts # Entry point, global API ├── index.ts # Entry point, global API (MetonaSqlark + MeSqlark)
├── core.ts # MarkdownEditor class ├── core.ts # MetonaSqlark main class
├── parser.ts # Markdown parser (tokenizer + renderer) ├── constants.ts # Types, defaults, enums, errors
├── plugins.ts # Plugin system & 6 presets ├── utils.ts # Utility functions
├── themes.ts # Theme system ├── engine/ # Storage engines
├── i18n.ts # Internationalization ├── interface.ts # IStorageEngine interface
├── styles.ts # CSS-in-JS injection │ ├── memory.ts # MemoryEngine (Map-based)
├── constants.ts # Types, defaults, configs │ ├── indexeddb.ts # IndexedDBEngine (browser persistence)
├── utils.ts # Utility functions │ └── opfs.ts # OPFSEngine (Origin Private File System)
├── animations.ts # Animation metadata ├── hybrid/ # HybridEngine (write-through)
├── icons.ts # Toolbar SVG icons ├── table/ # Table management & Schema validation
── locales.ts # Translation data ── query/ # Query system
│ ├── ast.ts # SQL AST type definitions
│ ├── builder.ts # Chainable QueryBuilder API
│ ├── compiler.ts # AST → QueryPlan compiler
│ ├── executor.ts # QueryExecutor with JOIN/GROUP BY support
│ └── where-matcher.ts # Unified WHERE matching logic
├── sql/ # SQL parser
│ ├── tokens.ts # Token types & keywords
│ ├── lexer.ts # Tokenizer
│ └── parser.ts # Recursive descent parser
├── transaction/ # Transaction manager
├── plugin/ # Plugin system (14 lifecycle hooks)
└── integrations/ # React & Vue hooks
tests/ tests/ # Test suite (264+ test cases, 15 test suites)
├── parser.test.ts site/ # Documentation site (index / docs / demo)
├── core.test.ts
├── plugins.test.ts
├── themes.test.ts
├── i18n.test.ts
├── utils.test.ts
├── animations.test.ts
├── index.test.ts
└── styles.test.ts
site/
├── index.html # Landing page
├── demo.html # Full-featured demo
└── docs.html # API documentation
``` ```
## Code Conventions ## Code Conventions
@@ -89,12 +88,43 @@ site/
### Commits ### Commits
- Use conventional commit messages: - Use conventional commit messages:
- `feat: add reference link resolution` - `feat: add SQL GROUP BY support`
- `fix: autoSave plugin state conflict` - `fix: JOIN with multiple ON conditions`
- `docs: update API reference` - `docs: update API reference`
- `test: add index.ts global API tests` - `test: add edge case coverage`
- `chore: optimize rollup dev build` - `chore: optimize rollup dev build`
## Architecture
MetonaSqlark follows a layered architecture:
```
┌──────────────────────────────────────────────────┐
│ MetonaSqlark / MeSqlark (Public API) │
├──────────────────────────────────────────────────┤
│ SQL Parser │ QueryBuilder │ Transaction │ Plugin │
│ (Lexer→AST) │ (.select…) │ Manager │ System │
├──────────────────────────────────────────────────┤
│ QueryExecutor (AST → Results) │
│ JOIN · GROUP BY · HAVING · DISTINCT · Agg │
├──────────────────────────────────────────────────┤
│ IStorageEngine (Interface) │
├─────────┬──────────┬──────────┬──────────────────┤
│ Memory │ IndexedDB │ OPFS │ Hybrid │
│ Engine │ Engine │ Engine │ Engine │
└─────────┴──────────┴──────────┴──────────────────┘
```
### Key Design Decisions
1. **AST as Universal Intermediate Representation**: Both SQL strings and Query Builder produce the same AST, ensuring consistent behavior regardless of API used.
2. **Write-Through Hybrid Strategy**: When using `mode: 'hybrid'`, all writes go to both memory and disk simultaneously. Reads always hit memory for maximum speed.
3. **Plugin Hook Pipeline**: 14 lifecycle hooks allow intercepting database operations without modifying core code.
4. **Hand-Written SQL Parser**: No dependencies on parser generators — a recursive-descent parser keeps the bundle size minimal.
## Building ## Building
```bash ```bash
@@ -102,11 +132,11 @@ site/
npm run build npm run build
# Output in dist/ # Output in dist/
# ├── metona-editor.js UMD # ├── metona-sqlark.js UMD
# ├── metona-editor.min.js UMD minified # ├── metona-sqlark.min.js UMD minified (~42KB / ~10KB gzip)
# ├── metona-editor.esm.js ES Module # ├── metona-sqlark.esm.js ES Module
# ├── metona-editor.cjs.js CommonJS # ├── metona-sqlark.cjs.js CommonJS
# └── metona-editor.d.ts TypeScript declarations # └── metona-sqlark.d.ts TypeScript declarations
``` ```
## Plugin Development ## Plugin Development
@@ -114,29 +144,58 @@ npm run build
Plugins follow a simple convention: Plugins follow a simple convention:
```typescript ```typescript
const myPlugin = { import type { MetonaPlugin } from '@metona-team/metona-sqlark';
const myPlugin: MetonaPlugin = {
name: 'myPlugin', name: 'myPlugin',
version: '1.0.0', version: '1.0.0',
description: 'Description of my plugin', description: 'Description of my plugin',
depends: [], // optional: plugin names this depends on priority: 50, // higher = executed first
priority: 50, // optional: for topological sort ordering
install(editor, options?) { install(db) {
// Called when plugin is installed // Use db.on() to subscribe to hooks
// Use editor.on() to subscribe to events db.on('beforeInsert', async (rows) => {
// Return a Promise for async initialization // Validate or transform data
});
db.on('afterQuery', async (sql, result) => {
// Log or cache query results
});
}, },
destroy(editor) { destroy() {
// Called when plugin is uninstalled // Clean up event listeners, timers, etc.
// Clean up event listeners, timers, DOM nodes
}, },
}; };
// Register in config
const db = await MetonaSqlark.create({
name: 'my-app',
plugins: [myPlugin],
});
``` ```
### Available Lifecycle Hooks
| Hook | Trigger | Parameters |
|------|---------|------------|
| `beforeCreateTable` | Before table creation | schema |
| `afterCreateTable` | After table creation | schema |
| `beforeDropTable` | Before table drop | tableName |
| `afterDropTable` | After table drop | tableName |
| `beforeInsert` | Before row insert | rows[] |
| `afterInsert` | After row insert | rows[] |
| `beforeUpdate` | Before row update | query, updates |
| `afterUpdate` | After row update | query, updates, count |
| `beforeDelete` | Before row delete | query |
| `afterDelete` | After row delete | query, count |
| `beforeQuery` | Before SQL query | sql |
| `afterQuery` | After SQL query | sql, result |
| `beforeTransaction` | Before transaction | - |
| `afterTransaction` | After transaction | - |
## Releasing ## Releasing
1. Update version in `package.json` and `src/index.ts` (`VERSION` constant). 1. Update version in `package.json` and `src/constants.ts` (`VERSION` constant).
2. Update `CHANGELOG.md`. 2. Update `CHANGELOG.md`.
3. Run full test suite: `npm test`. 3. Run full test suite: `npm test`.
4. Build: `npm run build`. 4. Build: `npm run build`.
@@ -144,4 +203,4 @@ const myPlugin = {
## Questions? ## Questions?
Open an issue at [git.metona.cn/MetonaTeam/MetonaEditor/issues](https://git.metona.cn/MetonaTeam/MetonaEditor/issues). Open an issue at [git.metona.cn/MetonaTeam/MetonaSqlark/issues](https://git.metona.cn/MetonaTeam/MetonaSqlark/issues).
+145 -89
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线演示 — MetonaSqlark</title> <title>在线演示 — MetonaSqlark v0.1.12</title>
<style> <style>
:root { :root {
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d; --bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
@@ -13,7 +13,6 @@
} }
* { margin:0; padding:0; box-sizing:border-box; } * { 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; } body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); height:100vh; overflow:hidden; }
/* Header */
header { header {
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px; 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; background:var(--surface); border-bottom:1px solid var(--border); z-index:10; position:relative;
@@ -27,7 +26,6 @@
.status { display:flex; align-items:center; gap:8px; font-size:0.82rem; color:var(--text2); } .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; } .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} } @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
/* Main Layout */
.main { display:flex; height:calc(100vh - 56px); } .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; } .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; } .result-panel { flex:1; display:flex; flex-direction:column; min-width:400px; }
@@ -40,7 +38,6 @@
font-size:0.7rem; padding:3px 10px; border-radius:999px; background:var(--surface2); font-size:0.7rem; padding:3px 10px; border-radius:999px; background:var(--surface2);
border:1px solid var(--border); text-transform:none; letter-spacing:0; border:1px solid var(--border); text-transform:none; letter-spacing:0;
} }
/* SQL Editor */
.editor-area { flex:1; display:flex; flex-direction:column; } .editor-area { flex:1; display:flex; flex-direction:column; }
#sql-input { #sql-input {
flex:1; background:var(--surface); color:var(--text); border:none; padding:20px; flex:1; background:var(--surface); color:var(--text); border:none; padding:20px;
@@ -48,7 +45,7 @@
resize:none; outline:none; caret-color:var(--primary); resize:none; outline:none; caret-color:var(--primary);
} }
#sql-input::placeholder { color:var(--text2); opacity:0.5; } #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); } .toolbar { display:flex; gap:8px; padding:12px 20px; border-top:1px solid var(--border); background:var(--surface2); flex-wrap:wrap; }
.btn { .btn {
display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border-radius:8px; 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; font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
@@ -60,7 +57,6 @@
.btn-clear:hover { color:var(--text); border-color:var(--text2); } .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 { 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); } .btn-preset:hover { background:rgba(6,182,212,0.12); }
/* Result */
.result-content { flex:1; overflow-y:auto; padding:20px; } .result-content { flex:1; overflow-y:auto; padding:20px; }
.result-table { width:100%; border-collapse:collapse; font-size:0.85rem; } .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 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; }
@@ -68,19 +64,12 @@
.result-table tr:hover td { background:rgba(99,102,241,0.04); } .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 { 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; } .empty-state .icon { font-size:3rem; opacity:0.3; }
/* Error */
.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; } .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; } .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; }
/* Tabs */ .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; }
.tabs { display:flex; gap:4px; padding:8px 20px 0; background:var(--surface); }
.tab { padding:8px 18px; border-radius:8px 8px 0 0; font-size:0.82rem; cursor:pointer; color:var(--text2); border:1px solid transparent; transition:.2s; }
.tab:hover { color:var(--text); }
.tab.active { color:var(--text); background:var(--bg); border-color:var(--border); border-bottom-color:var(--bg); }
/* Scrollbar */
::-webkit-scrollbar { width:6px; height:6px; } ::-webkit-scrollbar { width:6px; height:6px; }
::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; } ::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
/* Responsive */
@media(max-width:900px){ .main { flex-direction:column; } .editor-panel,.result-panel { min-width:auto; } .editor-panel { height:50%; } } @media(max-width:900px){ .main { flex-direction:column; } .editor-panel,.result-panel { min-width:auto; } .editor-panel { height:50%; } }
</style> </style>
</head> </head>
@@ -93,40 +82,40 @@
<a href="docs.html">文档</a> <a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a> <a href="demo.html" class="nav-active">演示</a>
</nav> </nav>
<div class="status"><span class="dot"></span> Memory 模式</div> <div class="status"><span class="dot"></span> Memory 模式 — v0.1.12</div>
</header> </header>
<div class="main"> <div class="main">
<!-- Editor Panel -->
<div class="editor-panel"> <div class="editor-panel">
<div class="tabs">
<div class="tab active" data-tab="sql">SQL 查询</div>
<div class="tab" data-tab="builder">Query Builder</div>
</div>
<div class="editor-area"> <div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users WHERE age > 18&#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 在线演示 🚀 <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.1.12 在线演示
-- 已预置 users / orders 表数据,试试以下查询: -- 已预置 users / orders / products 表数据
-- 查看所有数据
SELECT * FROM users; SELECT * FROM users;
-- 条件查询 + 排序
SELECT name, age FROM users WHERE age > 25 ORDER BY age DESC; SELECT name, age FROM users WHERE age > 25 ORDER BY age DESC;
SELECT u.name, o.amount, o.product -- JOIN 多表关联
SELECT u.name, o.product, o.amount
FROM users u INNER JOIN orders o ON u.id = o.user_id; 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> SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
<div class="toolbar"> <div class="toolbar">
<button class="btn btn-run" onclick="runQuery()">▶ 执行 (Ctrl+Enter)</button> <button class="btn btn-run" onclick="runQuery()">▶ 执行 (Ctrl+Enter)</button>
<button class="btn btn-clear" onclick="clearResults()">🗑 清空</button> <button class="btn btn-clear" onclick="clearResults()">🗑 清空</button>
<span style="flex:1"></span> <span style="flex:1"></span>
<button class="btn btn-preset" onclick="loadPreset('all')">📋 全数据</button> <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('join')">🔗 JOIN</button>
<button class="btn btn-preset" onclick="loadPreset('agg')">📊 聚合</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>
</div> </div>
</div> </div>
</div> </div>
<!-- Result Panel -->
<div class="result-panel"> <div class="result-panel">
<div class="panel-header"> <div class="panel-header">
<span>📊 查询结果</span> <span>📊 查询结果</span>
@@ -136,7 +125,7 @@ SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
<div class="empty-state"> <div class="empty-state">
<div class="icon"></div> <div class="icon"></div>
<div>执行 SQL 查询以查看结果</div> <div>执行 SQL 查询以查看结果</div>
<div style="font-size:0.8rem;opacity:0.6">Ctrl + Enter 快捷执行</div> <div style="font-size:0.8rem;opacity:0.6">Ctrl + Enter 快捷执行 · 支持多语句</div>
</div> </div>
</div> </div>
</div> </div>
@@ -144,7 +133,10 @@ SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
<script src="../dist/metona-sqlark.js"></script> <script src="../dist/metona-sqlark.js"></script>
<script> <script>
const { create, MetonaSqlark } = window.MetonaSqlark || window.MeSqlark || {}; // 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; let db = null;
const resultArea = document.getElementById('result-area'); const resultArea = document.getElementById('result-area');
@@ -153,53 +145,68 @@ const sqlInput = document.getElementById('sql-input');
// Init database // Init database
async function initDB() { async function initDB() {
db = new MetonaSqlark({ name: 'demo', mode: 'memory' }); try {
await db.init(); db = new DBClass({ name: 'demo', mode: 'memory' });
await db.init();
// Create tables // Create tables
await db.defineTable('users', { await db.defineTable('users', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true }, name: { type: 'string', required: true },
email: { type: 'string' }, email: { type: 'string' },
age: { type: 'number', default: 0 }, age: { type: 'number', default: 0 },
}); });
await db.defineTable('orders', { await db.defineTable('orders', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
user_id: { type: 'string' }, user_id: { type: 'string' },
product: { type: 'string' }, product: { type: 'string' },
amount: { type: 'number' }, 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 // Seed data
await db.table('users').insertMany([ await db.table('users').insertMany([
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 }, { id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 }, { id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 }, { id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
]); { id: '4', name: 'Diana', email: 'diana@demo.com', age: 28 },
await db.table('orders').insertMany([ { id: '5', name: 'Eve', email: 'eve@demo.com', age: 22 },
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200 }, ]);
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50 }, await db.table('orders').insertMany([
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150 }, { 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' },
]);
} catch (e) {
renderError('数据库初始化失败: ' + e.message);
}
} }
// Run SQL // Run SQL
async function runQuery() { async function runQuery() {
const sql = sqlInput.value.trim(); const sql = sqlInput.value.trim();
if (!sql) return; if (!sql || !db) return;
// Handle multiple statements
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0); const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
// For display purposes, if only one statement, use it; otherwise use first
try { try {
let lastResult = null;
const allResults = []; const allResults = [];
for (const stmt of statements) { for (const stmt of statements) {
const result = await db.query(stmt); const result = await db.query(stmt);
allResults.push({ sql: stmt, result }); allResults.push({ sql: stmt, result });
lastResult = result;
} }
if (allResults.length === 1) { if (allResults.length === 1) {
@@ -208,7 +215,7 @@ async function runQuery() {
renderAllResults(allResults); renderAllResults(allResults);
} }
} catch (e) { } catch (e) {
renderError(e.message); renderError(e.message || String(e));
} }
} }
@@ -223,7 +230,7 @@ function renderResult({ sql, result }) {
function renderAllResults(allResults) { function renderAllResults(allResults) {
let html = ''; let html = '';
for (const { sql, result } of allResults) { for (const { sql, result } of allResults) {
html += `<div style="color:var(--accent);font-size:0.78rem;margin-bottom:8px;font-family:monospace;">> ${sql}</div>`; html += `<div class="sql-tag">> ${escapeHTML(sql)}</div>`;
if (Array.isArray(result)) { if (Array.isArray(result)) {
if (result.length === 0) { if (result.length === 0) {
html += '<div class="success-box">✅ 查询返回 0 行</div>'; html += '<div class="success-box">✅ 查询返回 0 行</div>';
@@ -231,9 +238,9 @@ function renderAllResults(allResults) {
html += buildTableHTML(result); html += buildTableHTML(result);
} }
} else { } else {
html += `<div class="success-box">✅ 执行成功</div>`; html += `<div class="success-box">✅ 执行成功 (影响 ${result ?? 0} 行)</div>`;
} }
html += '<div style="height:16px"></div>'; html += '<div style="height:12px"></div>';
} }
resultArea.innerHTML = html; resultArea.innerHTML = html;
updateRowCount(allResults.reduce((sum, r) => sum + (Array.isArray(r.result) ? r.result.length : 0), 0)); updateRowCount(allResults.reduce((sum, r) => sum + (Array.isArray(r.result) ? r.result.length : 0), 0));
@@ -260,7 +267,7 @@ function buildTableHTML(rows) {
html += '<tr>'; html += '<tr>';
for (const col of columns) { for (const col of columns) {
const val = row[col]; const val = row[col];
html += `<td>${escapeHTML(val === null ? 'NULL' : String(val))}</td>`; html += `<td>${formatCell(val)}</td>`;
} }
html += '</tr>'; html += '</tr>';
} }
@@ -268,6 +275,14 @@ function buildTableHTML(rows) {
return html; 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) { function renderError(msg) {
resultArea.innerHTML = `<div class="error-box">❌ ${escapeHTML(msg)}</div>`; resultArea.innerHTML = `<div class="error-box">❌ ${escapeHTML(msg)}</div>`;
updateRowCount(0); updateRowCount(0);
@@ -282,32 +297,80 @@ function clearResults() {
updateRowCount(0); updateRowCount(0);
} }
function updateRowCount(n) { function updateRowCount(n) { rowCount.textContent = `${n}`; }
rowCount.textContent = `${n}`; function escapeHTML(str) { return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;'); }
}
function escapeHTML(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Presets // Presets
const presets = { const presets = {
all: `-- 查看所有用户 all: `-- 查看所有数据
SELECT * FROM users; SELECT * FROM users;
-- 查看所有订单 SELECT * FROM orders;
SELECT * FROM orders;`,
SELECT * FROM products;`,
join: `-- INNER JOIN:用户 + 订单 join: `-- INNER JOIN:用户 + 订单
SELECT u.name, o.product, o.amount SELECT u.name, o.product, o.amount, o.quantity
FROM users u FROM users u
INNER JOIN orders o ON u.id = o.user_id INNER JOIN orders o ON u.id = o.user_id
ORDER BY o.amount DESC;`, ORDER BY o.amount DESC;
agg: `-- 聚合:按用户统计订单总额
-- 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 SELECT u.name, COUNT(*) as orders, SUM(o.amount) as total_spent
FROM users u FROM users u
INNER JOIN orders o ON u.id = o.user_id INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.name GROUP BY u.name
ORDER BY total_spent DESC;` 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;`
}; };
function loadPreset(name) { function loadPreset(name) {
@@ -325,19 +388,12 @@ document.addEventListener('keydown', e => {
} }
}); });
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
});
});
// Boot // Boot
initDB().then(() => { initDB().then(() => {
console.log('✅ MetonaSqlark demo ready'); console.log('✅ MetonaSqlark v0.1.12 demo ready');
// Auto-run initial query setTimeout(runQuery, 300);
setTimeout(runQuery, 200); }).catch(err => {
renderError('初始化失败: ' + err.message);
}); });
</script> </script>
</body> </body>
+416 -104
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 — MetonaSqlark</title> <title>API 文档 — MetonaSqlark v0.1.12</title>
<style> <style>
:root { :root {
--bg:#0a0a0f; --surface:#14141f; --surface2:#1a1a2e; --border:#2a2a45; --bg:#0a0a0f; --surface:#14141f; --surface2:#1a1a2e; --border:#2a2a45;
@@ -13,7 +13,6 @@
} }
* { margin:0; padding:0; box-sizing:border-box; } * { 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.7; } body { font-family:'Inter',-apple-system,BlinkMacSystemFont,system-ui,sans-serif; background:var(--bg); color:var(--text); line-height:1.7; }
/* Header */
header { header {
position:fixed; top:0; left:0; right:0; z-index:100; backdrop-filter:blur(20px); position:fixed; top:0; left:0; right:0; z-index:100; backdrop-filter:blur(20px);
background:rgba(10,10,15,0.85); border-bottom:1px solid var(--border); padding:16px 32px; background:rgba(10,10,15,0.85); border-bottom:1px solid var(--border); padding:16px 32px;
@@ -25,7 +24,6 @@
nav { display:flex; gap:28px; } nav { display:flex; gap:28px; }
nav a { color:var(--text2); text-decoration:none; font-weight:500; font-size:0.92rem; transition:.2s; } nav a { color:var(--text2); text-decoration:none; font-weight:500; font-size:0.92rem; transition:.2s; }
nav a:hover,.nav-active { color:var(--text); } nav a:hover,.nav-active { color:var(--text); }
/* Layout */
.layout { display:flex; max-width:1400px; margin:0 auto; padding-top:80px; min-height:100vh; } .layout { display:flex; max-width:1400px; margin:0 auto; padding-top:80px; min-height:100vh; }
.sidebar { .sidebar {
width:260px; min-width:260px; padding:32px 24px; position:sticky; top:80px; height:calc(100vh - 80px); width:260px; min-width:260px; padding:32px 24px; position:sticky; top:80px; height:calc(100vh - 80px);
@@ -36,7 +34,7 @@
.sidebar a { display:block; padding:7px 12px; border-radius:6px; color:var(--text2); text-decoration:none; font-size:0.9rem; transition:.15s; } .sidebar a { display:block; padding:7px 12px; border-radius:6px; color:var(--text2); text-decoration:none; font-size:0.9rem; transition:.15s; }
.sidebar a:hover,.sidebar a.active { color:var(--text); background:var(--surface2); } .sidebar a:hover,.sidebar a.active { color:var(--text); background:var(--surface2); }
.sidebar a.active { border-left:3px solid var(--primary); } .sidebar a.active { border-left:3px solid var(--primary); }
.content { flex:1; padding:32px 48px 80px; max-width:900px; } .content { flex:1; padding:32px 48px 80px; max-width:950px; }
.content h2 { font-size:1.8rem; font-weight:800; margin:48px 0 16px; padding-top:24px; border-top:1px solid var(--border); } .content h2 { font-size:1.8rem; font-weight:800; margin:48px 0 16px; padding-top:24px; border-top:1px solid var(--border); }
.content h2:first-of-type { margin-top:0; padding-top:0; border-top:none; } .content h2:first-of-type { margin-top:0; padding-top:0; border-top:none; }
.content h3 { font-size:1.25rem; font-weight:700; margin:32px 0 12px; color:var(--primary-glow); } .content h3 { font-size:1.25rem; font-weight:700; margin:32px 0 12px; color:var(--primary-glow); }
@@ -53,13 +51,11 @@
.content pre .k { color:#c084fc; } .content pre .s { color:#34d399; } .content pre .k { color:#c084fc; } .content pre .s { color:#34d399; }
.content pre .f { color:#60a5fa; } .content pre .c { color:#64748b; } .content pre .f { color:#60a5fa; } .content pre .c { color:#64748b; }
.content pre .n { color:#fbbf24; } .content pre .t { color:#f472b6; } .content pre .n { color:#fbbf24; } .content pre .t { color:#f472b6; }
/* Table */
.content table { width:100%; border-collapse:collapse; margin:16px 0; } .content table { width:100%; border-collapse:collapse; margin:16px 0; }
.content th,.content td { text-align:left; padding:12px 16px; border-bottom:1px solid var(--border); font-size:0.9rem; } .content th,.content td { text-align:left; padding:12px 16px; border-bottom:1px solid var(--border); font-size:0.9rem; }
.content th { color:var(--text); font-weight:600; background:var(--surface); } .content th { color:var(--text); font-weight:600; background:var(--surface); }
.content td { color:var(--text2); } .content td { color:var(--text2); }
.content td code { font-size:0.82rem; } .content td code { font-size:0.82rem; }
/* Scrollbar */
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; } ::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
</style> </style>
</head> </head>
@@ -81,200 +77,516 @@
<h4>快速开始</h4> <h4>快速开始</h4>
<a href="#install" class="active">安装</a> <a href="#install" class="active">安装</a>
<a href="#create">创建数据库</a> <a href="#create">创建数据库</a>
<a href="#table">定义表</a> <a href="#define-table">定义表</a>
<h4>查询 API</h4> <h4>查询 API</h4>
<a href="#sql-query">SQL 查询</a> <a href="#sql-query">SQL 查询</a>
<a href="#query-builder">Query Builder</a> <a href="#query-builder">Query Builder</a>
<a href="#join">JOIN 查询</a> <a href="#join">JOIN 查询</a>
<a href="#groupby">GROUP BY</a> <a href="#groupby">GROUP BY & 聚合</a>
<a href="#where-ops">WHERE 操作符</a>
<h4>高级特性</h4> <h4>高级特性</h4>
<a href="#transaction">事务</a> <a href="#transaction">事务</a>
<a href="#migration">迁移</a> <a href="#migration">数据迁移</a>
<a href="#export">导入导出</a> <a href="#export">导入导出</a>
<a href="#plugin">插件钩子</a> <a href="#plugin">插件 & 钩子</a>
<a href="#subscribe">发布订阅</a> <a href="#subscribe">发布订阅</a>
<h4>类型</h4> <h4>框架集成</h4>
<a href="#react">React 集成</a>
<a href="#vue">Vue 集成</a>
<h4>类型参考</h4>
<a href="#types">TypeScript 类型</a> <a href="#types">TypeScript 类型</a>
<a href="#config">配置项</a> <a href="#config">完整配置项</a>
<a href="#engine">存储引擎</a>
<a href="#errors">错误处理</a>
</aside> </aside>
<main class="content"> <main class="content">
<h2 id="install">📦 安装</h2> <h2 id="install">📦 安装</h2>
<pre><span class="c">// npm</span> <p>MetonaSqlark 支持多种引入方式,覆盖 npm、CDN、ESM、CJS 所有常见场景。</p>
npm install @metona-team/MetonaSqlark
<span class="c">// ESM</span> <h3>npm 安装(推荐)</h3>
<span class="k">import</span> { MetonaSqlark, MeSqlark } <span class="k">from</span> <span class="s">'MetonaSqlark'</span>; <pre><span class="c"># 配置 Gitea Registry(一次性)</span>
npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
<span class="c">// Browser</span> <span class="c"># 安装</span>
&lt;script src=<span class="s">"metona-sqlark.min.js"</span>&gt;&lt;/script&gt; npm install @metona-team/metona-sqlark</pre>
<span class="c">// → window.MetonaSqlark / window.MeSqlark</span></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&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>
&lt;/script&gt;</pre>
<h3>ESM</h3>
<pre><span class="k">import</span> { MetonaSqlark, MeSqlark } <span class="k">from</span> <span class="s">'@metona-team/metona-sqlark'</span>;
<span class="c">// MeSqlark 是 MetonaSqlark 的别名,行为完全一致</span></pre>
<h3>CJS</h3>
<pre><span class="k">const</span> { MetonaSqlark } = <span class="f">require</span>(<span class="s">'@metona-team/metona-sqlark'</span>);</pre>
<p>输出文件说明:</p>
<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.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.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr>
</table>
<h2 id="create">🏗 创建数据库</h2> <h2 id="create">🏗 创建数据库</h2>
<p><code>MetonaSqlark.create(config)</code> 工厂函数,返回初始化好的实例。</p> <p><code>MetonaSqlark.create(config)</code> 工厂函数,自动创建并初始化数据库实例。</p>
<pre><span class="k">const</span> db = <span class="k">await</span> <span class="f">MetonaSqlark.create</span>({ <pre><span class="k">const</span> db = <span class="k">await</span> <span class="f">MetonaSqlark.create</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>, <span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">mode</span>: <span class="s">'hybrid'</span>, <span class="c">// 'memory' | 'disk' | 'hybrid'</span> <span class="s">mode</span>: <span class="s">'hybrid'</span>, <span class="c">// 'memory' | 'disk' | 'hybrid'</span>
<span class="s">diskEngine</span>: <span class="s">'indexeddb'</span>, <span class="c">// 'indexeddb' | 'opfs'</span> <span class="s">diskEngine</span>: <span class="s">'indexeddb'</span>, <span class="c">// 'indexeddb' | 'opfs'(仅 disk/hybrid 生效)</span>
<span class="s">version</span>: <span class="n">1</span>, <span class="s">version</span>: <span class="n">1</span>,
});</pre> <span class="s">plugins</span>: [], <span class="c">// MetonaPlugin[]</span>
<span class="s">onReady</span>: (db) => {}, <span class="c">// 就绪回调</span>
<span class="s">onError</span>: (err) => {}, <span class="c">// 错误回调</span>
});
<table> <span class="c">// 也可手动实例化</span>
<tr><th>属性</th><th>类型</th><th>默认</th><th>说明</th></tr> <span class="k">const</span> db2 = <span class="k">new</span> <span class="f">MetonaSqlark</span>(config);
<tr><td><code>name</code></td><td><code>string</code></td><td>-</td><td>数据库名称</td></tr> <span class="k">await</span> db2.<span class="f">init</span>();
<tr><td><code>mode</code></td><td><code>'memory'|'disk'|'hybrid'</code></td><td><code>'hybrid'</code></td><td>存储模式</td></tr>
<tr><td><code>diskEngine</code></td><td><code>'indexeddb'|'opfs'</code></td><td><code>'indexeddb'</code></td><td>磁盘引擎</td></tr> <span class="c">// 检查状态</span>
<tr><td><code>version</code></td><td><code>number</code></td><td><code>1</code></td><td>版本号</td></tr> db.<span class="f">isReady</span>(); <span class="c">// true</span>
</table>
<span class="c">// 关闭</span>
<span class="k">await</span> db.<span class="f">close</span>();</pre>
<h2 id="define-table">📋 定义表</h2>
<p>使用 <code>db.defineTable(name, columns)</code> 定义表结构。</p>
<h2 id="table">📋 定义表</h2>
<pre><span class="k">await</span> db.<span class="f">defineTable</span>(<span class="s">'users'</span>, { <pre><span class="k">await</span> db.<span class="f">defineTable</span>(<span class="s">'users'</span>, {
<span class="s">id</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">primaryKey</span>: <span class="k">true</span> }, <span class="s">id</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">primaryKey</span>: <span class="k">true</span> },
<span class="s">name</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">required</span>: <span class="k">true</span> }, <span class="s">name</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">required</span>: <span class="k">true</span> },
<span class="s">email</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">unique</span>: <span class="k">true</span>, <span class="s">index</span>: <span class="k">true</span> }, <span class="s">email</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">unique</span>: <span class="k">true</span>, <span class="s">index</span>: <span class="k">true</span> },
<span class="s">age</span>: { <span class="s">type</span>: <span class="s">'number'</span>, <span class="s">default</span>: <span class="n">0</span> }, <span class="s">age</span>: { <span class="s">type</span>: <span class="s">'number'</span>, <span class="s">default</span>: <span class="n">0</span>, <span class="s">min</span>: <span class="n">0</span>, <span class="s">max</span>: <span class="n">150</span> },
<span class="s">dept_id</span>: { <span class="s">type</span>: <span class="s">'number'</span>, <span class="s">references</span>: <span class="s">'departments.id'</span> }, <span class="s">active</span>: { <span class="s">type</span>: <span class="s">'boolean'</span>, <span class="s">default</span>: <span class="k">true</span> },
});</pre> <span class="s">birthday</span>: { <span class="s">type</span>: <span class="s">'date'</span> },
<span class="s">meta</span>: { <span class="s">type</span>: <span class="s">'json'</span> },
<span class="s">dept_id</span>: { <span class="s">type</span>: <span class="s">'number'</span>, <span class="s">references</span>: <span class="s">'departments.id'</span> },
});
<span class="c">// 表操作</span>
<span class="k">await</span> db.<span class="f">getTableNames</span>(); <span class="c">// ['users']</span>
<span class="k">await</span> db.<span class="f">dropTable</span>(<span class="s">'users'</span>); <span class="c">// 删除表</span></pre>
<table> <table>
<tr><th>字段</th><th>类型</th><th>说明</th></tr> <tr><th>ColumnDef 属性</th><th>类型</th><th>说明</th></tr>
<tr><td><code>type</code></td><td><code>string|number|boolean|date|json</code></td><td>数据类型</td></tr> <tr><td><code>type</code></td><td><code>'string'|'number'|'boolean'|'date'|'json'</code></td><td>数据类型</td></tr>
<tr><td><code>primaryKey</code></td><td><code>boolean</code></td><td>主键</td></tr> <tr><td><code>primaryKey</code></td><td><code>boolean</code></td><td>主键(每表至少一个)</td></tr>
<tr><td><code>required</code></td><td><code>boolean</code></td><td>必填</td></tr> <tr><td><code>required</code></td><td><code>boolean</code></td><td>是否必填</td></tr>
<tr><td><code>unique</code></td><td><code>boolean</code></td><td>唯一约束</td></tr> <tr><td><code>unique</code></td><td><code>boolean</code></td><td>唯一约束(自动建索引)</td></tr>
<tr><td><code>index</code></td><td><code>boolean</code></td><td>创建索引</td></tr> <tr><td><code>index</code></td><td><code>boolean</code></td><td>创建哈希索引,O(1) 加速查询</td></tr>
<tr><td><code>default</code></td><td><code>unknown</code></td><td>默认值</td></tr> <tr><td><code>default</code></td><td><code>unknown</code></td><td>默认值</td></tr>
<tr><td><code>references</code></td><td><code>string</code></td><td>外键引用</td></tr> <tr><td><code>maxLength</code></td><td><code>number</code></td><td>字符串最大长度</td></tr>
<tr><td><code>min</code>/<code>max</code></td><td><code>number</code></td><td>数值范围</td></tr>
<tr><td><code>references</code></td><td><code>string</code></td><td>外键引用 <code>'table.column'</code></td></tr>
</table> </table>
<h2 id="sql-query">🔍 SQL 查询</h2> <h2 id="sql-query">🔍 SQL 查询</h2>
<pre><span class="c">// INSERT</span> <p><code>db.query(sql)</code> — 执行标准 SQL 字符串,返回查询结果。</p>
<h3>完整 SQL 语法支持</h3>
<pre><span class="c">// SELECT — 核心查询</span>
<span class="k">const</span> rows = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT * FROM users
WHERE age > 18
ORDER BY name ASC
LIMIT 10 OFFSET 0`</span>);
<span class="c">// INSERT — 插入数据</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)"</span>); <span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)"</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO users VALUES ('2', 'Bob', 25)"</span>);
<span class="c">// 支持多行插入</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"INSERT INTO users VALUES ('3', 'C'), ('4', 'D')"</span>);
<span class="c">// SELECT</span> <span class="c">// UPDATE — 更新数据</span>
<span class="k">const</span> rows = <span class="k">await</span> db.<span class="f">query</span>(<span class="s">'SELECT * FROM users WHERE age > 18 ORDER BY name LIMIT 10'</span>); <span class="k">await</span> db.<span class="f">query</span>(<span class="s">"UPDATE users SET age = 31, active = true WHERE id = '1'"</span>);
<span class="c">// UPDATE</span> <span class="c">// DELETE — 删除数据</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"UPDATE users SET age = 31 WHERE id = '1'"</span>);
<span class="c">// DELETE</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"DELETE FROM users WHERE id = '1'"</span>); <span class="k">await</span> db.<span class="f">query</span>(<span class="s">"DELETE FROM users WHERE id = '1'"</span>);
<span class="c">// DDL</span> <span class="c">// DDL — 表结构操作</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP TABLE users'</span>);</pre> <span class="k">await</span> db.<span class="f">query</span>(<span class="s">`CREATE TABLE products (
id STRING PRIMARY KEY,
name STRING NOT NULL,
price NUMBER DEFAULT 0
)`</span>);
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP TABLE products'</span>);</pre>
<h3>条件表达式</h3>
<pre><span class="c">// 比较运算符</span>
<span class="s">`WHERE age > 18 AND name LIKE 'A%'`</span>
<span class="s">`WHERE salary >= 5000 OR dept = 'Engineering'`</span>
<span class="c">// IN / NOT IN</span>
<span class="s">`WHERE dept IN ('Engineering', 'Sales')`</span>
<span class="c">// NULL 检查</span>
<span class="s">`WHERE email IS NULL`</span>
<span class="s">`WHERE email IS NOT NULL`</span>
<span class="c">// NOT 取反</span>
<span class="s">`WHERE NOT (age < 18 OR age > 65)`</span>
<span class="c">// 嵌套条件</span>
<span class="s">`WHERE (age > 18 AND active = true) OR role = 'admin'`</span></pre>
<h2 id="query-builder">⛓ Query Builder</h2> <h2 id="query-builder">⛓ Query Builder</h2>
<p>链式 APITypeScript 友好,享受 IDE 自动补全。</p>
<h3>SELECT</h3>
<pre><span class="k">const</span> users = db.<span class="f">table</span>(<span class="s">'users'</span>); <pre><span class="k">const</span> users = db.<span class="f">table</span>(<span class="s">'users'</span>);
<span class="c">// 插入</span> <span class="c">// 全量查询</span>
<span class="k">await</span> users.<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'1'</span>, <span class="s">name</span>: <span class="s">'Alice'</span> }); <span class="k">await</span> users.<span class="f">select</span>().<span class="f">execute</span>();
<span class="k">await</span> users.<span class="f">insertMany</span>([...]);
<span class="c">// 查询</span> <span class="c">// 指定列 + 条件 + 排序 + 分页</span>
<span class="k">const</span> result = <span class="k">await</span> users <span class="k">const</span> result = <span class="k">await</span> users
.<span class="f">select</span>([<span class="s">'name'</span>, <span class="s">'age'</span>]) .<span class="f">select</span>([<span class="s">'name'</span>, <span class="s">'age'</span>, <span class="s">'email'</span>])
.<span class="f">where</span>({ <span class="s">age</span>: { <span class="s">$gt</span>: <span class="n">18</span> }, <span class="s">name</span>: { <span class="s">$like</span>: <span class="s">'A%'</span> } }) .<span class="f">where</span>({
<span class="s">age</span>: { <span class="s">$gt</span>: <span class="n">18</span> },
<span class="s">name</span>: { <span class="s">$like</span>: <span class="s">'A%'</span> },
})
.<span class="f">orderBy</span>(<span class="s">'age'</span>, <span class="s">'desc'</span>) .<span class="f">orderBy</span>(<span class="s">'age'</span>, <span class="s">'desc'</span>)
.<span class="f">limit</span>(<span class="n">10</span>).<span class="f">offset</span>(<span class="n">0</span>) .<span class="f">limit</span>(<span class="n">10</span>)
.<span class="f">execute</span>(); .<span class="f">offset</span>(<span class="n">0</span>)
.<span class="f">execute</span>();</pre>
<span class="c">// 更新/删除</span> <h3>INSERT</h3>
<span class="k">await</span> users.<span class="f">update</span>({ <span class="s">age</span>: <span class="n">31</span> }).<span class="f">where</span>({ <span class="s">id</span>: <span class="s">'1'</span> }).<span class="f">execute</span>(); <pre><span class="c">// 单行插入 — 返回主键值</span>
<span class="k">await</span> users.<span class="f">delete</span>().<span class="f">where</span>({ <span class="s">id</span>: <span class="s">'1'</span> }).<span class="f">execute</span>();</pre> <span class="k">const</span> pk = <span class="k">await</span> users.<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'1'</span>, <span class="s">name</span>: <span class="s">'Alice'</span>, <span class="s">age</span>: <span class="n">30</span> });
<span class="c">// 批量插入 — 返回主键值数组</span>
<span class="k">const</span> pks = <span class="k">await</span> users.<span class="f">insertMany</span>([
{ <span class="s">id</span>: <span class="s">'2'</span>, <span class="s">name</span>: <span class="s">'Bob'</span>, <span class="s">age</span>: <span class="n">25</span> },
{ <span class="s">id</span>: <span class="s">'3'</span>, <span class="s">name</span>: <span class="s">'Charlie'</span>, <span class="s">age</span>: <span class="n">35</span> },
]);</pre>
<h3>UPDATE / DELETE</h3>
<pre><span class="c">// 更新 — 返回影响行数</span>
<span class="k">const</span> updated = <span class="k">await</span> users
.<span class="f">update</span>({ <span class="s">age</span>: <span class="n">31</span>, <span class="s">active</span>: <span class="k">false</span> })
.<span class="f">where</span>({ <span class="s">id</span>: <span class="s">'1'</span> })
.<span class="f">execute</span>(); <span class="c">// 1</span>
<span class="c">// 删除 — 返回影响行数</span>
<span class="k">const</span> deleted = <span class="k">await</span> users
.<span class="f">delete</span>()
.<span class="f">where</span>({ <span class="s">id</span>: <span class="s">'1'</span> })
.<span class="f">execute</span>(); <span class="c">// 1</span>
<span class="c">// 全表删除</span>
<span class="k">await</span> users.<span class="f">delete</span>().<span class="f">execute</span>();</pre>
<h2 id="join">🔗 JOIN 查询</h2> <h2 id="join">🔗 JOIN 查询</h2>
<pre><span class="c">// SQL</span> <p>支持 INNER / LEFT / RIGHT / CROSS JOINSQL 和 Query Builder 两种方式。</p>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT u.name, d.name
<pre><span class="c">// SQL 方式 — 支持表别名</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT u.name, d.name AS dept_name
FROM users u FROM users u
INNER JOIN departments d ON u.dept_id = d.id INNER JOIN departments d ON u.dept_id = d.id
WHERE d.name = 'Engineering'`</span>); LEFT JOIN orders o ON u.id = o.user_id
WHERE d.name = 'Engineering'
ORDER BY u.name`</span>);
<span class="c">// QueryBuilder</span> <span class="c">// Query Builder 方式</span>
<span class="k">await</span> db.<span class="f">table</span>(<span class="s">'users'</span>).<span class="f">select</span>() <span class="k">await</span> db.<span class="f">table</span>(<span class="s">'users'</span>).<span class="f">select</span>()
.<span class="f">innerJoin</span>(<span class="s">'departments'</span>, { <span class="s">'users.dept_id'</span>: { <span class="s">$col</span>: <span class="s">'departments.id'</span> } }) .<span class="f">as</span>(<span class="s">'u'</span>)
.<span class="f">innerJoin</span>(<span class="s">'departments'</span>, { <span class="s">'u.dept_id'</span>: { <span class="s">$col</span>: <span class="s">'d.id'</span> } }, <span class="s">'d'</span>)
.<span class="f">leftJoin</span>(<span class="s">'orders'</span>, { <span class="s">'u.id'</span>: { <span class="s">$col</span>: <span class="s">'o.user_id'</span> } }, <span class="s">'o'</span>)
.<span class="f">execute</span>(); .<span class="f">execute</span>();
<span class="c">// LEFT JOIN / RIGHT JOIN / CROSS JOIN</span> <span class="c">// CROSS JOIN — 笛卡尔积</span>
.<span class="f">leftJoin</span>(<span class="s">'table'</span>, on) .<span class="f">crossJoin</span>(<span class="s">'metadata'</span>, <span class="s">'m'</span>)</pre>
.<span class="f">rightJoin</span>(<span class="s">'table'</span>, on)
.<span class="f">crossJoin</span>(<span class="s">'table'</span>)</pre>
<h2 id="groupby">📊 GROUP BY & 聚合</h2> <h2 id="groupby">📊 GROUP BY & 聚合</h2>
<pre><span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT dept, COUNT(*) as cnt, SUM(salary) as total <p>五大聚合函数 + GROUP BY + HAVING + DISTINCT,完整的数据分析能力。</p>
<pre><span class="c">// GROUP BY + 聚合</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT dept, COUNT(*) as cnt, SUM(salary) as total,
AVG(salary) as avg_sal, MIN(age), MAX(age)
FROM employees FROM employees
GROUP BY dept GROUP BY dept
HAVING COUNT(*) > 1 HAVING COUNT(*) > 1
ORDER BY total DESC`</span>); ORDER BY total DESC
LIMIT 5`</span>);
<span class="c">// 支持的聚合函数:COUNT, SUM, AVG, MIN, MAX</span> <span class="c">// DISTINCT 去重</span>
<span class="c">// 支持别名:COUNT(*) AS cnt</span></pre> <span class="k">await</span> db.<span class="f">query</span>(<span class="s">'SELECT DISTINCT dept FROM employees'</span>);
<span class="c">// 聚合函数别名</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'SELECT COUNT(*) AS total_users, AVG(age) AS avg_age FROM users'</span>);</pre>
<h2 id="where-ops">🎯 WHERE 操作符</h2>
<p>Query Builder 使用 <code>$</code> 前缀操作符,支持逻辑组合。</p>
<table>
<tr><th>操作符</th><th>含义</th><th>示例</th></tr>
<tr><td>直接值 / <code>$eq</code></td><td>等于</td><td><code>{ age: 30 }</code> / <code>{ age: { $eq: 30 } }</code></td></tr>
<tr><td><code>$ne</code></td><td>不等于</td><td><code>{ age: { $ne: 30 } }</code></td></tr>
<tr><td><code>$gt</code> / <code>$gte</code></td><td>大于 / 大于等于</td><td><code>{ age: { $gt: 18 } }</code></td></tr>
<tr><td><code>$lt</code> / <code>$lte</code></td><td>小于 / 小于等于</td><td><code>{ age: { $lt: 65 } }</code></td></tr>
<tr><td><code>$in</code> / <code>$nin</code></td><td>在列表中 / 不在</td><td><code>{ dept: { $in: ['IT','HR'] } }</code></td></tr>
<tr><td><code>$like</code></td><td>模糊匹配</td><td><code>{ name: { $like: 'A%' } }</code></td></tr>
<tr><td><code>$and</code></td><td>逻辑与</td><td><code>{ $and: [{...}, {...}] }</code></td></tr>
<tr><td><code>$or</code></td><td>逻辑或</td><td><code>{ $or: [{...}, {...}] }</code></td></tr>
<tr><td><code>$not</code></td><td>逻辑非</td><td><code>{ age: { $not: { $gt: 18 } } }</code></td></tr>
<tr><td><code>$col</code></td><td>列引用(JOIN ON</td><td><code>{ 'a.id': { $col: 'b.a_id' } }</code></td></tr>
</table>
<pre><span class="c">// 复合条件</span>
.<span class="f">where</span>({
<span class="s">$or</span>: [
{ <span class="s">age</span>: { <span class="s">$lt</span>: <span class="n">18</span> } },
{ <span class="s">age</span>: { <span class="s">$gt</span>: <span class="n">65</span> } },
],
<span class="s">active</span>: <span class="k">true</span>,
<span class="s">name</span>: { <span class="s">$like</span>: <span class="s">'A%'</span>, <span class="s">$ne</span>: <span class="s">'Admin'</span> },
})</pre>
<h2 id="transaction">🔒 事务</h2> <h2 id="transaction">🔒 事务</h2>
<p>保证原子性,事务内步骤失败自动报错。</p>
<pre><span class="k">await</span> db.<span class="f">transaction</span>(<span class="k">async</span> (trx) => { <pre><span class="k">await</span> db.<span class="f">transaction</span>(<span class="k">async</span> (trx) => {
<span class="c">// trx.table() 获取事务内表操作对象</span>
<span class="k">await</span> trx.<span class="f">table</span>(<span class="s">'users'</span>).<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'3'</span>, <span class="s">name</span>: <span class="s">'Charlie'</span> }); <span class="k">await</span> trx.<span class="f">table</span>(<span class="s">'users'</span>).<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'3'</span>, <span class="s">name</span>: <span class="s">'Charlie'</span> });
<span class="k">await</span> trx.<span class="f">table</span>(<span class="s">'orders'</span>).<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'o1'</span>, <span class="s">userId</span>: <span class="s">'3'</span>, <span class="s">amount</span>: <span class="n">99</span> }); <span class="k">await</span> trx.<span class="f">table</span>(<span class="s">'orders'</span>).<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'o1'</span>, <span class="s">userId</span>: <span class="s">'3'</span>, <span class="s">amount</span>: <span class="n">99</span> });
<span class="c">// 任何一步失败 → 全部回滚</span> <span class="k">await</span> trx.<span class="f">table</span>(<span class="s">'accounts'</span>).<span class="f">update</span>({ <span class="s">balance</span>: <span class="n">1</span> })
.<span class="f">where</span>({ <span class="s">userId</span>: <span class="s">'3'</span> })
.<span class="f">execute</span>();
<span class="c">// 也可返回结果</span>
<span class="k">return</span> <span class="s">'success'</span>;
});</pre> });</pre>
<h2 id="migration">🔄 数据迁移</h2> <h2 id="migration">🔄 数据迁移</h2>
<pre>db.<span class="f">addMigration</span>(<span class="n">2</span>, <span class="k">async</span> (db) => { <p>按版本号管理表结构变更。</p>
<span class="k">await</span> db.<span class="f">defineTable</span>(<span class="s">'products'</span>, { ... });
<pre><span class="c">// 注册迁移</span>
db.<span class="f">addMigration</span>(<span class="n">2</span>, <span class="k">async</span> (db) => {
<span class="k">await</span> db.<span class="f">defineTable</span>(<span class="s">'products'</span>, {
<span class="s">id</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">primaryKey</span>: <span class="k">true</span> },
<span class="s">name</span>: { <span class="s">type</span>: <span class="s">'string'</span>, <span class="s">required</span>: <span class="k">true</span> },
});
}); });
<span class="k">await</span> db.<span class="f">migrateTo</span>(<span class="n">2</span>); <span class="c">// 执行所有未执行的迁移</span></pre>
db.<span class="f">addMigration</span>(<span class="n">3</span>, <span class="k">async</span> (db) => {
<span class="c">// 添加新列、数据迁移等</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">"UPDATE users SET role = 'user' WHERE role IS NULL"</span>);
});
<span class="c">// 执行迁移到目标版本</span>
<span class="k">await</span> db.<span class="f">migrateTo</span>(<span class="n">3</span>); <span class="c">// 依次执行 v2, v3 的迁移函数</span></pre>
<h2 id="export">📤 导入导出</h2> <h2 id="export">📤 导入导出</h2>
<pre><span class="c">// 导出单表</span> <pre><span class="c">// 导出单表 — 返回 JSON 数组</span>
<span class="k">const</span> data = <span class="k">await</span> db.<span class="f">exportTable</span>(<span class="s">'users'</span>); <span class="k">const</span> userData = <span class="k">await</span> db.<span class="f">exportTable</span>(<span class="s">'users'</span>);
<span class="c">// [{ id: '1', name: 'Alice', ... }, ...]</span>
<span class="c">// 导出全库</span> <span class="c">// 导出全库 — 返回 { tableName: rows[] }</span>
<span class="k">const</span> all = <span class="k">await</span> db.<span class="f">exportAll</span>(); <span class="k">const</span> allData = <span class="k">await</span> db.<span class="f">exportAll</span>();
<span class="c">// { users: [...], orders: [...], products: [...] }</span>
<span class="c">// 导入</span> <span class="c">// 导入数据 — 返回主键列表</span>
<span class="k">await</span> db.<span class="f">importTable</span>(<span class="s">'users'</span>, data);</pre> <span class="k">const</span> pks = <span class="k">await</span> db.<span class="f">importTable</span>(<span class="s">'users'</span>, userData);</pre>
<h2 id="plugin">🧩 插件钩子</h2> <h2 id="plugin">🧩 插件 & 钩子</h2>
<pre><span class="c">// 14 生命周期钩子</span> <p>14 生命周期钩子,支持插件机制。</p>
<table>
<tr><th>钩子名称</th><th>触发时机</th><th>参数</th></tr>
<tr><td><code>beforeCreateTable</code></td><td>创建表前</td><td>schema</td></tr>
<tr><td><code>afterCreateTable</code></td><td>创建表后</td><td>schema</td></tr>
<tr><td><code>beforeDropTable</code></td><td>删除表前</td><td>tableName</td></tr>
<tr><td><code>afterDropTable</code></td><td>删除表后</td><td>tableName</td></tr>
<tr><td><code>beforeInsert</code></td><td>插入前</td><td>rows[]</td></tr>
<tr><td><code>afterInsert</code></td><td>插入后</td><td>rows[]</td></tr>
<tr><td><code>beforeUpdate</code></td><td>更新前</td><td>query, updates</td></tr>
<tr><td><code>afterUpdate</code></td><td>更新后</td><td>query, updates, count</td></tr>
<tr><td><code>beforeDelete</code></td><td>删除前</td><td>query</td></tr>
<tr><td><code>afterDelete</code></td><td>删除后</td><td>query, count</td></tr>
<tr><td><code>beforeQuery</code></td><td>SQL 查询前</td><td>sql</td></tr>
<tr><td><code>afterQuery</code></td><td>SQL 查询后</td><td>sql, result</td></tr>
<tr><td><code>beforeTransaction</code></td><td>事务开始前</td><td>-</td></tr>
<tr><td><code>afterTransaction</code></td><td>事务完成后</td><td>-</td></tr>
</table>
<pre><span class="c">// 注册钩子</span>
db.<span class="f">on</span>(<span class="s">'beforeInsert'</span>, <span class="k">async</span> (row) => { db.<span class="f">on</span>(<span class="s">'beforeInsert'</span>, <span class="k">async</span> (row) => {
<span class="f">console.log</span>(<span class="s">'即将插入:'</span>, row); <span class="f">console.log</span>(<span class="s">'即将插入:'</span>, row);
<span class="c">// 可在此校验、转换数据</span>
}); });
db.<span class="f">on</span>(<span class="s">'afterQuery'</span>, <span class="k">async</span> (sql, result) => { db.<span class="f">on</span>(<span class="s">'afterQuery'</span>, <span class="k">async</span> (sql, result) => {
<span class="f">console.log</span>(<span class="s">'查询完成:'</span>, sql); <span class="f">console.log</span>(<span class="s">`查询完成 [</span><span class="k">${sql}</span><span class="s">] → </span><span class="k">${(result as any[]).length}</span><span class="s"> 行`</span>);
});
<span class="c">// 注册自定义插件</span>
<span class="k">const</span> loggerPlugin = {
<span class="s">name</span>: <span class="s">'logger'</span>,
<span class="s">version</span>: <span class="s">'1.0.0'</span>,
<span class="s">description</span>: <span class="s">'记录所有数据库操作'</span>,
<span class="s">priority</span>: <span class="n">100</span>,
<span class="f">install</span>(db) {
db.<span class="f">on</span>(<span class="s">'beforeQuery'</span>, (sql) => <span class="f">console.log</span>(<span class="s">'SQL:'</span>, sql));
},
<span class="f">destroy</span>() { <span class="c">/* 清理 */</span> },
};
<span class="c">// 在 create 配置中注册</span>
<span class="k">const</span> db = <span class="k">await</span> <span class="f">MetonaSqlark.create</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">plugins</span>: [loggerPlugin],
});</pre> });</pre>
<table>
<tr><th>钩子</th><th>触发时机</th></tr>
<tr><td><code>beforeCreateTable / afterCreateTable</code></td><td>创建表前后</td></tr>
<tr><td><code>beforeDropTable / afterDropTable</code></td><td>删除表前后</td></tr>
<tr><td><code>beforeInsert / afterInsert</code></td><td>插入前后</td></tr>
<tr><td><code>beforeUpdate / afterUpdate</code></td><td>更新前后</td></tr>
<tr><td><code>beforeDelete / afterDelete</code></td><td>删除前后</td></tr>
<tr><td><code>beforeQuery / afterQuery</code></td><td>查询前后</td></tr>
<tr><td><code>beforeTransaction / afterTransaction</code></td><td>事务前后</td></tr>
</table>
<h2 id="subscribe">📡 发布订阅</h2> <h2 id="subscribe">📡 发布订阅</h2>
<pre><span class="c">// 订阅表变更</span> <pre><span class="c">// 订阅表变更</span>
<span class="k">const</span> unsubscribe = db.<span class="f">subscribe</span>(<span class="s">'users'</span>, (event) => { <span class="k">const</span> unsubscribe = db.<span class="f">subscribe</span>(<span class="s">'users'</span>, (event) => {
<span class="c">// event: { type: 'insert'|'update'|'delete', row: {...} }</span> <span class="c">// event.type: 'insert' | 'update' | 'delete'</span>
<span class="c">// event.row: 被操作的行数据</span>
<span class="f">console.log</span>(<span class="s">`users 表 </span><span class="k">${event.type}</span><span class="s">`</span>, event.row);
}); });
<span class="c">// 手动触发变更</span>
db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s">type</span>: <span class="s">'insert'</span>, <span class="s">row</span>: { <span class="s">id</span>: <span class="s">'1'</span>, <span class="s">name</span>: <span class="s">'Alice'</span> } });
<span class="c">// 取消订阅</span> <span class="c">// 取消订阅</span>
<span class="f">unsubscribe</span>();</pre> <span class="f">unsubscribe</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>;
<span class="k">function</span> <span class="f">UserList</span>() {
<span class="c">// 执行 SQL 查询,自动响应 db 变化</span>
<span class="k">const</span> { data, loading, error, refresh } = <span class="f">useQuery</span>(
db,
<span class="s">'SELECT * FROM users WHERE age > 18'</span>,
[<span class="c">/* deps */</span>]
);
<span class="k">if</span> (loading) <span class="k">return</span> &lt;div&gt;Loading...&lt;/div&gt;;
<span class="k">if</span> (error) <span class="k">return</span> &lt;div&gt;Error: {error.message}&lt;/div&gt;;
<span class="k">return</span> (
&lt;div&gt;
{data.<span class="f">map</span>(u =&gt; &lt;div key={u.id}&gt;{u.name} ({u.age})&lt;/div&gt;)}
&lt;button onClick={refresh}&gt;刷新&lt;/button&gt;
&lt;/div&gt;
);
}
<span class="c">// 便捷 hook — 查询整张表</span>
<span class="k">const</span> { data, loading, refresh } = <span class="f">useTable</span>(db, <span class="s">'users'</span>);
<span class="c">// 管理数据库生命周期</span>
<span class="k">function</span> <span class="f">App</span>() {
<span class="k">const</span> { db, ready, error } = <span class="f">useDatabase</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">mode</span>: <span class="s">'hybrid'</span>,
});
<span class="k">if</span> (!ready) <span class="k">return</span> &lt;div&gt;Initializing...&lt;/div&gt;;
<span class="k">return</span> &lt;UserList /&gt;;
}</pre>
<h2 id="vue">🟢 Vue 集成</h2>
<pre><span class="k">import</span> { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase }
<span class="k">from</span> <span class="s">'@metona-team/metona-sqlark/vue'</span>;
<span class="k">import</span> { db } <span class="k">from</span> <span class="s">'./db'</span>;
<span class="c">// useSqlarkQuery — 执行 SQL 查询</span>
<span class="k">const</span> { data, loading, error, refresh } = <span class="f">useSqlarkQuery</span>(
db,
<span class="s">'SELECT * FROM users WHERE age > 18'</span>
);
<span class="c">// useSqlarkTable — 获取全表数据</span>
<span class="k">const</span> { data, loading, refresh } = <span class="f">useSqlarkTable</span>(db, <span class="s">'users'</span>);
<span class="c">// useSqlarkDatabase — 管理数据库生命周期</span>
<span class="k">const</span> { db, ready, error } = <span class="f">useSqlarkDatabase</span>({
<span class="s">name</span>: <span class="s">'my-app'</span>,
<span class="s">mode</span>: <span class="s">'hybrid'</span>,
});</pre>
<h2 id="types">🔷 TypeScript 泛型</h2> <h2 id="types">🔷 TypeScript 泛型</h2>
<pre><span class="k">interface</span> <span class="t">User</span> { <pre><span class="k">interface</span> <span class="t">User</span> {
<span class="s">id</span>: <span class="t">string</span>; <span class="s">id</span>: <span class="t">string</span>;
<span class="s">name</span>: <span class="t">string</span>; <span class="s">name</span>: <span class="t">string</span>;
<span class="s">age</span>: <span class="t">number</span>; <span class="s">age</span>: <span class="t">number</span>;
<span class="s">email</span>?: <span class="t">string</span>;
} }
<span class="c">// 泛型表操作 — 类型安全的 insert/select</span>
<span class="k">const</span> users = db.<span class="f">table</span>&lt;<span class="t">User</span>&gt;(<span class="s">'users'</span>); <span class="k">const</span> users = db.<span class="f">table</span>&lt;<span class="t">User</span>&gt;(<span class="s">'users'</span>);
<span class="k">await</span> users.<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'1'</span>, <span class="s">name</span>: <span class="s">'Alice'</span>, <span class="s">age</span>: <span class="n">30</span> }); <span class="c">// ✅ 类型安全</span></pre>
<h2 id="config">⚙️ 配置项</h2> <span class="c">// ✅ 类型检查通过</span>
<pre><span class="k">import</span> { MetonaSqlark, MeSqlark } <span class="k">from</span> <span class="s">'MetonaSqlark'</span>; <span class="k">await</span> users.<span class="f">insert</span>({ <span class="s">id</span>: <span class="s">'1'</span>, <span class="s">name</span>: <span class="s">'Alice'</span>, <span class="s">age</span>: <span class="n">30</span> });
<span class="c">// MeSqlark 是 MetonaSqlark 的别名,完全等价</span></pre>
<span class="c">// ❌ TypeScript 报错:缺少 name</span>
<span class="c">// await users.insert({ id: '2', age: 25 });</span></pre>
<h2 id="config">⚙️ 完整配置项</h2>
<table>
<tr><th>属性</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td><code>name</code></td><td><code>string</code></td><td><code>'metona-sqlark'</code></td><td>数据库名称(必填)</td></tr>
<tr><td><code>mode</code></td><td><code>'memory'|'disk'|'hybrid'</code></td><td><code>'hybrid'</code></td><td>存储模式</td></tr>
<tr><td><code>diskEngine</code></td><td><code>'indexeddb'|'opfs'</code></td><td><code>'indexeddb'</code></td><td>磁盘引擎类型</td></tr>
<tr><td><code>version</code></td><td><code>number</code></td><td><code>1</code></td><td>数据库版本号</td></tr>
<tr><td><code>plugins</code></td><td><code>MetonaPlugin[]</code></td><td><code>[]</code></td><td>初始插件列表</td></tr>
<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>错误回调</td></tr>
</table>
<h2 id="engine">💾 存储引擎</h2>
<table>
<tr><th>引擎</th><th>模式</th><th>持久化</th><th>性能</th><th>适用场景</th></tr>
<tr><td><code>MemoryEngine</code></td><td>memory</td><td>❌ 否</td><td>⚡ 极快</td><td>临时数据、缓存、测试</td></tr>
<tr><td><code>IndexedDBEngine</code></td><td>disk</td><td>✅ 是</td><td>🚀 快</td><td>通用持久化,兼容性最好</td></tr>
<tr><td><code>OPFSEngine</code></td><td>disk</td><td>✅ 是</td><td>🚀 快</td><td>现代浏览器,文件级存储</td></tr>
<tr><td><code>HybridEngine</code></td><td>hybrid</td><td>✅ 是</td><td>⚡ 极快</td><td>生产推荐,读写均走内存</td></tr>
</table>
<p>Hybrid 引擎采用 <strong>write-through</strong> 策略:所有写操作同时写入内存和磁盘,所有读操作直接从内存返回,启动时从磁盘加载数据到内存。</p>
<h2 id="errors">⚠️ 错误处理</h2>
<p>所有错误抛出 <code>DatabaseError</code> 实例。</p>
<pre><span class="k">try</span> {
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'SELECT * FROM nonexistent'</span>);
} <span class="k">catch</span> (err) {
<span class="k">if</span> (err <span class="k">instanceof</span> <span class="t">DatabaseError</span>) {
<span class="f">console.log</span>(err.message); <span class="c">// 'Table "nonexistent" does not exist'</span>
<span class="f">console.log</span>(err.code); <span class="c">// 'TABLE_NOT_FOUND'</span>
<span class="f">console.log</span>(err.details); <span class="c">// 附加信息</span>
}
}</pre>
<table>
<tr><th>错误码</th><th>触发场景</th></tr>
<tr><td><code>TABLE_NOT_FOUND</code></td><td>表不存在</td></tr>
<tr><td><code>TABLE_EXISTS</code></td><td>表已存在</td></tr>
<tr><td><code>DUPLICATE_KEY</code></td><td>主键重复</td></tr>
<tr><td><code>UNIQUE_VIOLATION</code></td><td>唯一约束冲突</td></tr>
<tr><td><code>VALIDATION_ERROR</code></td><td>数据校验失败</td></tr>
<tr><td><code>TYPE_ERROR</code></td><td>字段类型错误</td></tr>
<tr><td><code>SCHEMA_ERROR</code></td><td>表结构定义错误</td></tr>
<tr><td><code>DB_NOT_READY</code></td><td>数据库未初始化</td></tr>
<tr><td><code>PARSE_ERROR</code></td><td>SQL 语法错误</td></tr>
<tr><td><code>TRANSACTION_ERROR</code></td><td>事务执行失败</td></tr>
<tr><td><code>COMPILE_ERROR</code></td><td>编译 AST 到查询计划失败</td></tr>
<tr><td><code>CONFIG_ERROR</code></td><td>配置错误</td></tr>
</table>
</main> </main>
</div> </div>
+81 -19
View File
@@ -151,7 +151,7 @@
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.1.11 已发布</div> <div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.1.12 已发布</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1> <h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,内存与磁盘双模式,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。</p> <p>TypeScript 原生构建,内存与磁盘双模式,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。</p>
<div class="actions"> <div class="actions">
@@ -232,63 +232,123 @@ npm install @metona-team/metona-sqlark
<div class="feature-card"> <div class="feature-card">
<div class="icon">🧠</div> <div class="icon">🧠</div>
<h3>双模式存储</h3> <h3>双模式存储</h3>
<p>Memory(内存)保证极致速度,DiskIndexedDB / OPFS)提供持久化。Hybrid 模式一键兼顾</p> <p>Memory(内存)保证极致速度,DiskIndexedDB / OPFS)提供持久化。Hybrid 模式 write-through 策略,读写均在微秒级完成</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon"></div> <div class="icon"></div>
<h3>完整 SQL 支持</h3> <h3>完整 SQL 解析器</h3>
<p>SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / DISTINCT — 手写递归下降解析器</p> <p>手写递归下降 SQL 解析器,无第三方依赖。支持 SELECT / INSERT / UPDATE / DELETE / CREATE TABLE / DROP TABLE / JOIN / GROUP BY / HAVING / DISTINCT。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">🔗</div> <div class="icon">🔗</div>
<h3>Query Builder</h3> <h3>Query Builder</h3>
<p>链式 APITypeScript 友好。.select().where().innerJoin().orderBy().limit() — 代码即查询</p> <p>链式 API + TypeScript 泛型支持。.select().where().innerJoin().orderBy().limit() — IDE 自动补全,类型安全</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">🔒</div> <div class="icon">🔒</div>
<h3>事务支持</h3> <h3>事务支持</h3>
<p>原子性操作,事务内任意步骤失败自动回滚。IndexedDB 引擎利用浏览器原生事务。</p> <p>原子性操作,事务内任意步骤失败自动回滚。IndexedDB 引擎原生事务 + Memory 引擎模拟事务。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">🧩</div> <div class="icon">🧩</div>
<h3>插件系统</h3> <h3>插件系统</h3>
<p>14 种生命周期钩子,注册自定义插件。beforeQuery / afterInsert / beforeTransaction……</p> <p>14 种生命周期钩子beforeCreateTable / afterInsert / beforeQuery / afterTransaction……按优先级注册,完整生命周期管理。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">📦</div> <div class="icon">📦</div>
<h3>零运行时依赖</h3> <h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakable,最小体积不到 10KB gzip。</p> <p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,最小体积 ~10KB gzip。</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>内置版本迁移系统,addMigration + migrateTo API。导入导出支持单表/全库 JSON 序列化。</p>
</div>
<div class="feature-card">
<div class="icon">📡</div>
<h3>发布订阅</h3>
<p>subscribe() 订阅表变更事件,emit() 触发通知。React useQuery / Vue useSqlarkQuery 开箱即用。</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Code Example --> <!-- 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 │
├─────────────────────────────────────────────────────┤
<span style="color:#34d399;">IStorageEngine</span> Interface │
├──────────┬──────────┬──────────┬───────────────────┤
<span style="color:#fbbf24;">Memory </span><span style="color:#fbbf24;">IndexedDB </span><span style="color:#fbbf24;">OPFS </span><span style="color:#fbbf24;">Hybrid </span>
│ Engine │ Engine │ Engine │ Engine │
│ (Map) │ (IDB) │ (File) │ (Write-through) │
└──────────┴──────────┴──────────┴───────────────────┘
</pre>
</div>
</div>
</section>
<section style="background:var(--gradient2);"> <section style="background:var(--gradient2);">
<div class="container"> <div class="container">
<div class="section-title"> <div class="section-title">
<h2>5 行<span>代码</span>开始</h2> <h2>5 行<span>代码</span>开始</h2>
<p>像写 SQL 一样操作前端数据</p> <p>像写 SQL 一样操作前端数据 — 或使用 Query Builder 链式 API</p>
</div> </div>
<div class="code-block"> <div class="code-block">
<pre><span class="comment">// 创建数据库</span> <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="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="comment">// 定义表结构</span>
<span class="keyword">await</span> db.<span class="func">defineTable</span>(<span class="string">'users'</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">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">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">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="comment">// SQL 查询</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">"INSERT INTO users VALUES ('1', 'Alice', 30)"</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'</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="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="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="func">where</span>({ <span class="string">age</span>: { <span class="string">$gt</span>: <span class="number">18</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>();</pre> .<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> });
});</pre>
</div> </div>
</div> </div>
</section> </section>
@@ -301,10 +361,12 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p> <p>MetonaSqlark 的核心指标</p>
</div> </div>
<div class="stats"> <div class="stats">
<div class="stat-card"><div class="num">235+</div><div class="label">测试用例</div></div> <div class="stat-card"><div class="num">264+</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">78.9%</div><div class="label">代码覆盖率</div></div> <div class="stat-card"><div class="num">93.5%</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">~10KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">4</div><div class="label">存储引擎</div></div> <div class="stat-card"><div class="num">4</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">29</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">15</div><div class="label">测试套件</div></div>
</div> </div>
</div> </div>
</section> </section>