feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库

- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid
- 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT
- Query Builder链式API + TypeScript泛型支持
- 聚合函数:COUNT/SUM/AVG/MIN/MAX
- 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出
- React/Vue框架集成
- 264个测试用例,93.46%覆盖率
- 零运行时依赖
This commit is contained in:
thzxx
2026-07-26 15:00:01 +08:00
commit e2a590c5b1
60 changed files with 16359 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true,
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"],
"rules": {
"no-console": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"no-undef": "off",
"no-empty": "off",
"no-useless-escape": "off",
"no-control-regex": "off",
"no-regex-spaces": "off",
"no-constant-condition": ["warn", { "checkLoops": false }]
},
"ignorePatterns": ["dist/", "coverage/", "node_modules/"]
}
+42
View File
@@ -0,0 +1,42 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: debian-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
continue-on-error: true
- name: Run tests
run: npx jest --forceExit --maxWorkers=2 --no-cache
env:
NODE_OPTIONS: --max-old-space-size=4096
- name: Build
run: npm run build
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
coverage/
.DS_Store
# 本地 npm 发布凭据(含 _auth,勿提交)
.npmrc
+15
View File
@@ -0,0 +1,15 @@
# Changelog
All notable changes to YOUR-PROJECT will be documented in this file.
## [0.0.1] - 2026-07-25
### Added
- 初始项目骨架
- TypeScript 配置(严格模式)
- Rollup 构建(UMD / ESM / CJS / min / .d.ts
- Jest + jsdom 测试环境
- ESLint + @typescript-eslint
- Gitea Actions CI 工作流
- 示例站点(index / demo / docs
- 构建脚本 build.sh / serve.sh
+147
View File
@@ -0,0 +1,147 @@
# Contributing to MetonaEditor
Thanks for your interest in contributing! This document outlines the development workflow and conventions.
## Prerequisites
- Node.js >= 16.0.0
- npm >= 8.0.0
## Setup
```bash
git clone https://git.metona.cn/MetonaTeam/MetonaEditor.git
cd MetonaEditor
npm install
```
## Development
```bash
# Start dev server with hot reload (port 3001)
npm run dev
# Run tests in watch mode
npm run test:watch
# Type check
npm run typecheck
# Lint
npm run lint
npm run lint:fix
# Format
npm run format
```
## Project Structure
```
src/
├── index.ts # Entry point, global API
├── core.ts # MarkdownEditor class
├── parser.ts # Markdown parser (tokenizer + renderer)
├── plugins.ts # Plugin system & 6 presets
├── themes.ts # Theme system
├── i18n.ts # Internationalization
├── styles.ts # CSS-in-JS injection
├── constants.ts # Types, defaults, configs
├── utils.ts # Utility functions
├── animations.ts # Animation metadata
├── icons.ts # Toolbar SVG icons
└── locales.ts # Translation data
tests/
├── parser.test.ts
├── 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
### TypeScript
- **Strict mode** is enabled — all code must pass `tsc --noEmit`.
- Export types explicitly. Avoid `any` where possible.
- Use `interface` for object shapes, `type` for unions/primitives.
### Style
- Run `npm run format` before committing (uses Prettier).
- Follow existing comment patterns: JSDoc `/** */` for public APIs, `//` for inline notes.
- Keep functions focused and under ~60 lines where practical.
### Testing
- **Every new feature must include tests.**
- Test files mirror source structure: `src/foo.ts``tests/foo.test.ts`.
- Use descriptive test names: `('does X when Y')`.
- Run the full suite before submitting: `npm test`.
### Commits
- Use conventional commit messages:
- `feat: add reference link resolution`
- `fix: autoSave plugin state conflict`
- `docs: update API reference`
- `test: add index.ts global API tests`
- `chore: optimize rollup dev build`
## Building
```bash
# Production build (all formats)
npm run build
# Output in dist/
# ├── metona-editor.js UMD
# ├── metona-editor.min.js UMD minified
# ├── metona-editor.esm.js ES Module
# ├── metona-editor.cjs.js CommonJS
# └── metona-editor.d.ts TypeScript declarations
```
## Plugin Development
Plugins follow a simple convention:
```typescript
const myPlugin = {
name: 'myPlugin',
version: '1.0.0',
description: 'Description of my plugin',
depends: [], // optional: plugin names this depends on
priority: 50, // optional: for topological sort ordering
install(editor, options?) {
// Called when plugin is installed
// Use editor.on() to subscribe to events
// Return a Promise for async initialization
},
destroy(editor) {
// Called when plugin is uninstalled
// Clean up event listeners, timers, DOM nodes
},
};
```
## Releasing
1. Update version in `package.json` and `src/index.ts` (`VERSION` constant).
2. Update `CHANGELOG.md`.
3. Run full test suite: `npm test`.
4. Build: `npm run build`.
5. Publish: `npm publish`.
## Questions?
Open an issue at [git.metona.cn/MetonaTeam/MetonaEditor/issues](https://git.metona.cn/MetonaTeam/MetonaEditor/issues).
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 thzxx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+184
View File
@@ -0,0 +1,184 @@
# metona-sqlark
<p align="center">
<img src="https://img.shields.io/badge/version-0.1.12-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-93.46%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-264%20passed-success?style=flat-square" alt="tests">
</p>
> 基于 TypeScript 的**前端关系型数据库**,内存与磁盘双模式运行,支持完整 SQL 查询与 Query Builder 链式 API。
---
## ✨ 核心特性
- 🧠 **双模式存储** — Memory / Disk / Hybrid,内存极速 + 磁盘持久化
-**完整 SQL** — SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / DISTINCT
- 🔗 **Query Builder** — 链式 APITypeScript 类型友好
- 📊 **聚合函数** — COUNT / SUM / AVG / MIN / MAX
- 🔒 **事务支持** — 原子性操作,失败自动回滚
- 🧩 **插件系统** — 14 种生命周期钩子
- 📡 **发布订阅** — 表级变更监听
- 🔄 **数据迁移** — 版本化 migration 系统
- 📤 **导入导出** — JSON 格式全库/单表导入导出
- ⚛️ **React / Vue** — 原生 hooks & composables
- 📦 **零运行时依赖** — 纯 TypeScript,约 42KB min+gzip
---
## 📦 安装
```bash
npm install @metona-team/metona-sqlark
```
```typescript
// ESM / TypeScript
import { MetonaSqlark, MeSqlark } from '@metona-team/metona-sqlark';
// Browser UMD
<script src="metona-sqlark.min.js"></script>
// → window.MetonaSqlark / window.MeSqlark
```
---
## 🚀 快速开始
```typescript
import { MetonaSqlark } from '@metona-team/metona-sqlark';
// 创建数据库
const db = await MetonaSqlark.create({
name: 'my-app',
mode: 'hybrid',
diskEngine: 'indexeddb',
});
// 定义表
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true, index: true },
});
// SQL 查询
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30, 'alice@demo.com')");
const rows = await db.query('SELECT * FROM users WHERE age > 18 ORDER BY name');
// Query Builder
const users = db.table('users');
await users.insert({ id: '2', name: 'Bob', age: 25, email: 'bob@demo.com' });
const result = await users.select().where({ age: { $gt: 18 } }).orderBy('age', 'desc').execute();
// JOIN
await db.query(`SELECT u.name, o.product FROM users u INNER JOIN orders o ON u.id = o.user_id`);
// GROUP BY
await db.query(`SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1`);
// 事务
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '3', name: 'Charlie' });
await trx.table('orders').insert({ id: 'o1', userId: '3', amount: 99 });
});
```
---
## 📖 API 速览
### 数据库配置
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `name` | `string` | `'metona-sqlark'` | 数据库名称 |
| `mode` | `'memory' \| 'disk' \| 'hybrid'` | `'hybrid'` | 存储模式 |
| `diskEngine` | `'indexeddb' \| 'opfs'` | `'indexeddb'` | 磁盘引擎 |
| `version` | `number` | `1` | 版本号 |
### WHERE 操作符
| 操作符 | 含义 | 操作符 | 含义 |
|--------|------|--------|------|
| `$eq` / 直接值 | 等于 | `$gt` / `$gte` | 大于 / 大于等于 |
| `$ne` | 不等于 | `$lt` / `$lte` | 小于 / 小于等于 |
| `$in` / `$nin` | 在列表中 | `$like` | 模糊匹配 |
| `$and` / `$or` / `$not` | 逻辑组合 | | |
### 核心方法
| 方法 | 说明 |
|------|------|
| `db.query(sql)` | 执行 SQL 字符串 |
| `db.table(name)` | 获取表操作对象 |
| `db.defineTable(name, cols)` | 定义表结构 |
| `db.transaction(fn)` | 执行事务 |
| `db.exportTable(name)` / `db.exportAll()` | 导出数据 |
| `db.addMigration(v, fn)` / `db.migrateTo(v)` | 数据迁移 |
| `db.subscribe(table, fn)` | 订阅表变更 |
| `db.on(hook, fn)` | 注册钩子 |
### React / Vue 集成
```tsx
// React
import { useQuery } from '@metona-team/metona-sqlark/react';
const { data, loading, refresh } = useQuery(db, 'SELECT * FROM users');
// Vue
import { useSqlarkQuery } from '@metona-team/metona-sqlark/vue';
const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
```
---
## 📊 项目状态
| 指标 | 数值 |
|------|------|
| 测试用例 | 264 |
| 测试套件 | 15 |
| 语句覆盖率 | 93.46% |
| SQL 关键字 | 29 |
| 存储引擎 | 4Memory / IndexedDB / OPFS / Hybrid |
---
## 🛠 开发
```bash
npm install # 安装依赖
npm run dev # 开发模式(localhost:3001
npm run build # 生产构建
npm test # 运行测试
npm run lint # 代码检查
npm run typecheck # 类型检查
```
---
## 📂 项目结构
```
src/
├── index.ts # 入口(MetonaSqlark + MeSqlark
├── core.ts # 主类
├── constants.ts # 类型定义 + 配置
├── engine/ # 存储引擎(Memory/IndexedDB/OPFS
├── hybrid/ # 混合引擎
├── table/ # 表管理 + Schema 校验
├── query/ # AST + Builder + Compiler + Executor
├── sql/ # Lexer + Parser
├── transaction/ # 事务管理
├── plugin/ # 插件系统
└── integrations/ # React / Vue hooks
```
---
## 📄 License
MIT © [MetonaTeam](https://git.metona.cn/MetonaTeam)
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# 构建脚本 — 安装依赖 → 测试 → 构建
echo "🔨 构建脚本"
if ! command -v node &> /dev/null; then
echo "❌ 未找到 Node.js"
exit 1
fi
echo "✅ Node.js: $(node -v)"
echo "📦 安装依赖..."
npm install || exit 1
echo "🧪 运行测试..."
npm test || exit 1
echo "🔨 构建..."
npm run build || exit 1
echo ""
echo "✅ 构建完成"
ls -lh dist/
+23
View File
@@ -0,0 +1,23 @@
module.exports = {
testEnvironment: 'jsdom',
setupFiles: ['./jest.setup.js'],
transform: {
'^.+\\.ts$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/**/index.ts',
'!src/query/ast.ts',
'!src/engine/interface.ts',
'!src/engine/opfs.ts',
'!src/integrations/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
verbose: true,
};
+4
View File
@@ -0,0 +1,4 @@
// jest setup: polyfill structuredClone for fake-indexeddb
if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
}
+8111
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
{
"name": "@metona-team/metona-sqlark",
"version": "0.1.12",
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module",
"main": "dist/metona-sqlark.js",
"module": "src/index.ts",
"unpkg": "dist/metona-sqlark.min.js",
"jsdelivr": "dist/metona-sqlark.min.js",
"types": "dist/metona-sqlark.d.ts",
"exports": {
".": {
"import": "./src/index.ts",
"require": "./dist/metona-sqlark.js",
"types": "./dist/metona-sqlark.d.ts"
}
},
"files": [
"dist/",
"src/",
"README.md",
"LICENSE"
],
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "jest --coverage",
"test:watch": "jest --watch",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\"",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run typecheck && npm test && npm run build"
},
"repository": {
"type": "git",
"url": "git+https://git.metona.cn/MetonaTeam/metona-sqlark.git"
},
"keywords": [
"database",
"sql",
"frontend",
"indexeddb",
"opfs",
"memory",
"browser",
"typescript"
],
"author": "thzxx",
"license": "MIT",
"bugs": {
"url": "https://git.metona.cn/MetonaTeam/metona-sqlark/issues"
},
"homepage": "https://git.metona.cn/MetonaTeam/metona-sqlark#readme",
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.1.0",
"@types/jest": "^29.5.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.5.0",
"eslint": "^8.40.0",
"fake-indexeddb": "^6.2.5",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.0",
"rollup": "^3.20.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^2.0.1",
"tslib": "^2.6.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=16.0.0"
},
"publishConfig": {
"registry": "https://git.metona.cn/api/packages/MetonaTeam/npm/"
}
}
+103
View File
@@ -0,0 +1,103 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
const isDev = process.env.ROLLUP_WATCH;
const isProd = process.env.NODE_ENV === 'production';
const basePlugins = [
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: false,
}),
];
const devPlugins = isDev ? [
serve({
open: true,
contentBase: ['site', 'dist'],
port: 3001,
}),
livereload({
watch: ['src', 'site'],
}),
] : [];
export default [
// UMD development (always built)
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.js',
format: 'umd',
name: 'MetonaSqlark',
exports: 'named',
sourcemap: true,
},
plugins: [...basePlugins, ...devPlugins],
},
// Only build all formats in production
...(isDev ? [] : [
// ES Module
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.esm.js',
format: 'es',
exports: 'named',
sourcemap: true,
},
plugins: basePlugins,
},
// CommonJS
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.cjs.js',
format: 'cjs',
exports: 'named',
sourcemap: true,
},
plugins: basePlugins,
},
// UMD minified
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.min.js',
format: 'umd',
name: 'MetonaSqlark',
exports: 'named',
sourcemap: false,
},
plugins: [
...basePlugins,
terser({
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn'],
passes: 2,
},
format: { comments: false },
mangle: { toplevel: true },
}),
],
},
// TypeScript declarations bundle
{
input: 'src/index.ts',
output: {
file: 'dist/metona-sqlark.d.ts',
format: 'es',
},
plugins: [dts()],
},
]),
];
+344
View File
@@ -0,0 +1,344 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线演示 — metona-sqlark</title>
<style>
:root {
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
--primary:#6366f1; --accent:#06b6d4; --accent2:#ec4899; --green:#22c55e;
--text:#e2e8f0; --text2:#8b949e; --radius:12px;
--gradient:linear-gradient(135deg,#6366f1,#06b6d4,#ec4899);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); height:100vh; overflow:hidden; }
/* Header */
header {
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
background:var(--surface); border-bottom:1px solid var(--border); z-index:10; position:relative;
}
.logo { font-weight:800; font-size:1.1rem; display:flex; align-items:center; gap:10px; }
.logo .icon { width:28px; height:28px; border-radius:7px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:900; color:#fff; }
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:20px; }
nav a { color:var(--text2); text-decoration:none; font-size:0.88rem; transition:.2s; }
nav a:hover,.nav-active { color:var(--text); }
.status { display:flex; align-items:center; gap:8px; font-size:0.82rem; color:var(--text2); }
.status .dot { width:7px; height:7px; border-radius:50%; background:var(--green); animation:pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
/* Main Layout */
.main { display:flex; height:calc(100vh - 56px); }
.editor-panel { flex:1; display:flex; flex-direction:column; border-right:1px solid var(--border); min-width:400px; }
.result-panel { flex:1; display:flex; flex-direction:column; min-width:400px; }
.panel-header {
padding:12px 20px; font-size:0.78rem; font-weight:600; text-transform:uppercase;
letter-spacing:1px; color:var(--text2); border-bottom:1px solid var(--border);
display:flex; align-items:center; justify-content:space-between;
}
.panel-header .badge {
font-size:0.7rem; padding:3px 10px; border-radius:999px; background:var(--surface2);
border:1px solid var(--border); text-transform:none; letter-spacing:0;
}
/* SQL Editor */
.editor-area { flex:1; display:flex; flex-direction:column; }
#sql-input {
flex:1; background:var(--surface); color:var(--text); border:none; padding:20px;
font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.9rem; line-height:1.8;
resize:none; outline:none; caret-color:var(--primary);
}
#sql-input::placeholder { color:var(--text2); opacity:0.5; }
.toolbar { display:flex; gap:8px; padding:12px 20px; border-top:1px solid var(--border); background:var(--surface2); }
.btn {
display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border-radius:8px;
font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
}
.btn-run { background:var(--gradient); color:#fff; box-shadow:0 4px 15px rgba(99,102,241,0.35); }
.btn-run:hover { transform:translateY(-1px); box-shadow:0 6px 25px rgba(99,102,241,0.5); }
.btn-run:active { transform:scale(0.97); }
.btn-clear { background:var(--surface2); color:var(--text2); border:1px solid var(--border); }
.btn-clear:hover { color:var(--text); border-color:var(--text2); }
.btn-preset { background:transparent; color:var(--accent); border:1px solid var(--accent); font-size:0.78rem; padding:6px 14px; }
.btn-preset:hover { background:rgba(6,182,212,0.12); }
/* Result */
.result-content { flex:1; overflow-y:auto; padding:20px; }
.result-table { width:100%; border-collapse:collapse; font-size:0.85rem; }
.result-table th { text-align:left; padding:10px 14px; background:var(--surface2); color:var(--accent); font-weight:600; border-bottom:2px solid var(--border); position:sticky; top:0; z-index:1; }
.result-table td { padding:8px 14px; border-bottom:1px solid var(--border); color:var(--text2); max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.result-table tr:hover td { background:rgba(99,102,241,0.04); }
.empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text2); gap:12px; }
.empty-state .icon { font-size:3rem; opacity:0.3; }
/* Error */
.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; }
/* Tabs */
.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-track { background:transparent; }
::-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%; } }
</style>
</head>
<body>
<header>
<div class="logo"><div class="icon"></div><span>metona-sqlark</span></div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a>
</nav>
<div class="status"><span class="dot"></span> Memory 模式</div>
</header>
<div class="main">
<!-- 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">
<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">-- 欢迎使用 metona-sqlark 在线演示 🚀
-- 已预置 users / orders 表数据,试试以下查询:
SELECT * FROM users;
SELECT name, age FROM users WHERE age > 25 ORDER BY age DESC;
SELECT u.name, o.amount, o.product
FROM users u INNER JOIN orders o ON u.id = o.user_id;
SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
<div class="toolbar">
<button class="btn btn-run" onclick="runQuery()">▶ 执行 (Ctrl+Enter)</button>
<button class="btn btn-clear" onclick="clearResults()">🗑 清空</button>
<span style="flex:1"></span>
<button class="btn btn-preset" onclick="loadPreset('all')">📋 全部数据</button>
<button class="btn btn-preset" onclick="loadPreset('join')">🔗 JOIN</button>
<button class="btn btn-preset" onclick="loadPreset('agg')">📊 聚合</button>
</div>
</div>
</div>
<!-- Result Panel -->
<div class="result-panel">
<div class="panel-header">
<span>📊 查询结果</span>
<span class="badge" id="row-count">0 行</span>
</div>
<div class="result-content" id="result-area">
<div class="empty-state">
<div class="icon"></div>
<div>执行 SQL 查询以查看结果</div>
<div style="font-size:0.8rem;opacity:0.6">Ctrl + Enter 快捷执行</div>
</div>
</div>
</div>
</div>
<script src="../dist/metona-sqlark.js"></script>
<script>
const { create, MetonaSqlark } = window.MetonaSqlark || window.MeSqlark || {};
let db = null;
const resultArea = document.getElementById('result-area');
const rowCount = document.getElementById('row-count');
const sqlInput = document.getElementById('sql-input');
// Init database
async function initDB() {
db = new MetonaSqlark({ name: 'demo', mode: 'memory' });
await db.init();
// Create tables
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
email: { type: 'string' },
age: { type: 'number', default: 0 },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
product: { type: 'string' },
amount: { type: 'number' },
});
// Seed data
await db.table('users').insertMany([
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
]);
await db.table('orders').insertMany([
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200 },
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50 },
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150 },
]);
}
// Run SQL
async function runQuery() {
const sql = sqlInput.value.trim();
if (!sql) return;
// Handle multiple statements
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 {
let lastResult = null;
const allResults = [];
for (const stmt of statements) {
const result = await db.query(stmt);
allResults.push({ sql: stmt, result });
lastResult = result;
}
if (allResults.length === 1) {
renderResult(allResults[0]);
} else {
renderAllResults(allResults);
}
} catch (e) {
renderError(e.message);
}
}
function renderResult({ sql, result }) {
if (Array.isArray(result)) {
renderTable(result);
} else {
renderSuccess(`✅ 执行成功 (影响 ${result ?? 0} 行)`);
}
}
function renderAllResults(allResults) {
let html = '';
for (const { sql, result } of allResults) {
html += `<div style="color:var(--accent);font-size:0.78rem;margin-bottom:8px;font-family:monospace;">> ${sql}</div>`;
if (Array.isArray(result)) {
if (result.length === 0) {
html += '<div class="success-box">✅ 查询返回 0 行</div>';
} else {
html += buildTableHTML(result);
}
} else {
html += `<div class="success-box">✅ 执行成功</div>`;
}
html += '<div style="height:16px"></div>';
}
resultArea.innerHTML = html;
updateRowCount(allResults.reduce((sum, r) => sum + (Array.isArray(r.result) ? r.result.length : 0), 0));
}
function renderTable(rows) {
if (!rows || rows.length === 0) {
resultArea.innerHTML = '<div class="success-box">✅ 查询返回 0 行</div>';
updateRowCount(0);
return;
}
resultArea.innerHTML = buildTableHTML(rows);
updateRowCount(rows.length);
}
function buildTableHTML(rows) {
const columns = Object.keys(rows[0]);
let html = '<table class="result-table"><thead><tr>';
for (const col of columns) {
html += `<th>${escapeHTML(col)}</th>`;
}
html += '</tr></thead><tbody>';
for (const row of rows) {
html += '<tr>';
for (const col of columns) {
const val = row[col];
html += `<td>${escapeHTML(val === null ? 'NULL' : String(val))}</td>`;
}
html += '</tr>';
}
html += '</tbody></table>';
return html;
}
function renderError(msg) {
resultArea.innerHTML = `<div class="error-box">❌ ${escapeHTML(msg)}</div>`;
updateRowCount(0);
}
function renderSuccess(msg) {
resultArea.innerHTML = `<div class="success-box">${msg}</div>`;
}
function clearResults() {
resultArea.innerHTML = `<div class="empty-state"><div class="icon">⚡</div><div>结果已清空</div></div>`;
updateRowCount(0);
}
function updateRowCount(n) {
rowCount.textContent = `${n}`;
}
function escapeHTML(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Presets
const presets = {
all: `-- 查看所有用户
SELECT * FROM users;
-- 查看所有订单
SELECT * FROM orders;`,
join: `-- INNER JOIN:用户 + 订单
SELECT u.name, o.product, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
ORDER BY o.amount DESC;`,
agg: `-- 聚合:按用户统计订单总额
SELECT u.name, COUNT(*) as orders, SUM(o.amount) as total_spent
FROM users u
INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.name
ORDER BY total_spent DESC;`
};
function loadPreset(name) {
if (presets[name]) {
sqlInput.value = presets[name];
runQuery();
}
}
// Keyboard shortcut
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
runQuery();
}
});
// 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
initDB().then(() => {
console.log('✅ metona-sqlark demo ready');
// Auto-run initial query
setTimeout(runQuery, 200);
});
</script>
</body>
</html>
+291
View File
@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 — metona-sqlark</title>
<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%);
--radius:14px;
}
* { 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; }
/* Header */
header {
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;
}
header .inner { max-width:1400px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; }
.logo { font-size:1.3rem; font-weight:800; display:flex; align-items:center; gap:10px; }
.logo .icon { width:32px; height:32px; border-radius:8px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:1rem; font-weight:900; color:#fff; }
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:28px; }
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); }
/* Layout */
.layout { display:flex; max-width:1400px; margin:0 auto; padding-top:80px; min-height:100vh; }
.sidebar {
width:260px; min-width:260px; padding:32px 24px; position:sticky; top:80px; height:calc(100vh - 80px);
overflow-y:auto; border-right:1px solid var(--border); background:var(--bg);
}
.sidebar h4 { font-size:0.75rem; text-transform:uppercase; letter-spacing:1px; color:var(--text2); margin:20px 0 8px; }
.sidebar h4:first-child { margin-top:0; }
.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.active { border-left:3px solid var(--primary); }
.content { flex:1; padding:32px 48px 80px; max-width:900px; }
.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 h3 { font-size:1.25rem; font-weight:700; margin:32px 0 12px; color:var(--primary-glow); }
.content p { color:var(--text2); margin-bottom:16px; }
.content code {
background:var(--surface2); padding:2px 8px; border-radius:5px; font-family:'JetBrains Mono',monospace;
font-size:0.88rem; color:var(--accent);
}
.content pre {
background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius);
padding:20px 24px; overflow-x:auto; font-family:'JetBrains Mono',monospace; font-size:0.85rem;
line-height:1.65; margin:16px 0; color:#e2e8f0;
}
.content pre .k { color:#c084fc; } .content pre .s { color:#34d399; }
.content pre .f { color:#60a5fa; } .content pre .c { color:#64748b; }
.content pre .n { color:#fbbf24; } .content pre .t { color:#f472b6; }
/* Table */
.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 { color:var(--text); font-weight:600; background:var(--surface); }
.content td { color:var(--text2); }
.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; }
</style>
</head>
<body>
<header>
<div class="inner">
<div class="logo"><div class="icon"></div><span>metona-sqlark</span></div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html" class="nav-active">文档</a>
<a href="demo.html">演示</a>
</nav>
</div>
</header>
<div class="layout">
<aside class="sidebar">
<h4>快速开始</h4>
<a href="#install" class="active">安装</a>
<a href="#create">创建数据库</a>
<a href="#table">定义表</a>
<h4>查询 API</h4>
<a href="#sql-query">SQL 查询</a>
<a href="#query-builder">Query Builder</a>
<a href="#join">JOIN 查询</a>
<a href="#groupby">GROUP BY</a>
<h4>高级特性</h4>
<a href="#transaction">事务</a>
<a href="#migration">迁移</a>
<a href="#export">导入导出</a>
<a href="#plugin">插件钩子</a>
<a href="#subscribe">发布订阅</a>
<h4>类型</h4>
<a href="#types">TypeScript 类型</a>
<a href="#config">配置项</a>
</aside>
<main class="content">
<h2 id="install">📦 安装</h2>
<pre><span class="c">// npm</span>
npm install @metona-team/metona-sqlark
<span class="c">// ESM</span>
<span class="k">import</span> { MetonaSqlark, MeSqlark } <span class="k">from</span> <span class="s">'metona-sqlark'</span>;
<span class="c">// Browser</span>
&lt;script src=<span class="s">"metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
<span class="c">// → window.MetonaSqlark / window.MeSqlark</span></pre>
<h2 id="create">🏗 创建数据库</h2>
<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>({
<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">diskEngine</span>: <span class="s">'indexeddb'</span>, <span class="c">// 'indexeddb' | 'opfs'</span>
<span class="s">version</span>: <span class="n">1</span>,
});</pre>
<table>
<tr><th>属性</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td><code>name</code></td><td><code>string</code></td><td>-</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>
</table>
<h2 id="table">📋 定义表</h2>
<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">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">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">dept_id</span>: { <span class="s">type</span>: <span class="s">'number'</span>, <span class="s">references</span>: <span class="s">'departments.id'</span> },
});</pre>
<table>
<tr><th>字段</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>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>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>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>
</table>
<h2 id="sql-query">🔍 SQL 查询</h2>
<pre><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="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 LIMIT 10'</span>);
<span class="c">// UPDATE</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="c">// DDL</span>
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP TABLE users'</span>);</pre>
<h2 id="query-builder">⛓ Query Builder</h2>
<pre><span class="k">const</span> users = db.<span class="f">table</span>(<span class="s">'users'</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">insertMany</span>([...]);
<span class="c">// 查询</span>
<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">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">limit</span>(<span class="n">10</span>).<span class="f">offset</span>(<span class="n">0</span>)
.<span class="f">execute</span>();
<span class="c">// 更新/删除</span>
<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>();
<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>
<h2 id="join">🔗 JOIN 查询</h2>
<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
FROM users u
INNER JOIN departments d ON u.dept_id = d.id
WHERE d.name = 'Engineering'`</span>);
<span class="c">// QueryBuilder</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">execute</span>();
<span class="c">// LEFT JOIN / RIGHT JOIN / CROSS JOIN</span>
.<span class="f">leftJoin</span>(<span class="s">'table'</span>, on)
.<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>
<pre><span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT dept, COUNT(*) as cnt, SUM(salary) as total
FROM employees
GROUP BY dept
HAVING COUNT(*) > 1
ORDER BY total DESC`</span>);
<span class="c">// 支持的聚合函数:COUNT, SUM, AVG, MIN, MAX</span>
<span class="c">// 支持别名:COUNT(*) AS cnt</span></pre>
<h2 id="transaction">🔒 事务</h2>
<pre><span class="k">await</span> db.<span class="f">transaction</span>(<span class="k">async</span> (trx) => {
<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="c">// 任何一步失败 → 全部回滚</span>
});</pre>
<h2 id="migration">🔄 数据迁移</h2>
<pre>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="k">await</span> db.<span class="f">migrateTo</span>(<span class="n">2</span>); <span class="c">// 执行所有未执行的迁移</span></pre>
<h2 id="export">📤 导入导出</h2>
<pre><span class="c">// 导出单表</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="c">// 导出全库</span>
<span class="k">const</span> all = <span class="k">await</span> db.<span class="f">exportAll</span>();
<span class="c">// 导入</span>
<span class="k">await</span> db.<span class="f">importTable</span>(<span class="s">'users'</span>, data);</pre>
<h2 id="plugin">🧩 插件钩子</h2>
<pre><span class="c">// 14 个生命周期钩子</span>
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);
});
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);
});</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>
<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="c">// event: { type: 'insert'|'update'|'delete', row: {...} }</span>
});
<span class="c">// 取消订阅</span>
<span class="f">unsubscribe</span>();</pre>
<h2 id="types">🔷 TypeScript 泛型</h2>
<pre><span class="k">interface</span> <span class="t">User</span> {
<span class="s">id</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="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>
<pre><span class="k">import</span> { MetonaSqlark, MeSqlark } <span class="k">from</span> <span class="s">'metona-sqlark'</span>;
<span class="c">// MeSqlark 是 MetonaSqlark 的别名,完全等价</span></pre>
</main>
</div>
<script>
document.querySelectorAll('.sidebar a').forEach(a => {
a.addEventListener('click', function() {
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
this.classList.add('active');
});
});
</script>
</body>
</html>
+319
View File
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>metona-sqlark — 前端 TypeScript 数据库</title>
<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>metona-sqlark</span>
</div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="https://git.metona.cn/MetonaTeam/metona-sqlark" target="_blank">GitHub</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.1.11 已发布</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,内存与磁盘双模式,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。</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>
<!-- Features -->
<section>
<div class="container">
<div class="section-title">
<h2>为什么选择 <span>metona-sqlark</span></h2>
<p>专为前端打造的数据库引擎</p>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="icon">🧠</div>
<h3>双模式存储</h3>
<p>Memory(内存)保证极致速度,DiskIndexedDB / OPFS)提供持久化。Hybrid 模式一键兼顾。</p>
</div>
<div class="feature-card">
<div class="icon"></div>
<h3>完整 SQL 支持</h3>
<p>SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / DISTINCT — 手写递归下降解析器。</p>
</div>
<div class="feature-card">
<div class="icon">🔗</div>
<h3>Query Builder</h3>
<p>链式 APITypeScript 友好。.select().where().innerJoin().orderBy().limit() — 代码即查询。</p>
</div>
<div class="feature-card">
<div class="icon">🔒</div>
<h3>事务支持</h3>
<p>原子性操作,事务内任意步骤失败自动回滚。IndexedDB 引擎利用浏览器原生事务。</p>
</div>
<div class="feature-card">
<div class="icon">🧩</div>
<h3>插件系统</h3>
<p>14 种生命周期钩子,注册自定义插件。beforeQuery / afterInsert / beforeTransaction……</p>
</div>
<div class="feature-card">
<div class="icon">📦</div>
<h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakable,最小体积不到 10KB gzip。</p>
</div>
</div>
</div>
</section>
<!-- Code Example -->
<section style="background:var(--gradient2);">
<div class="container">
<div class="section-title">
<h2>5 行<span>代码</span>开始</h2>
<p>像写 SQL 一样操作前端数据</p>
</div>
<div class="code-block">
<pre><span class="comment">// 创建数据库</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="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">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="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="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>
</div>
</div>
</section>
<!-- Stats -->
<section>
<div class="container">
<div class="section-title">
<h2><span>数字</span>说话</h2>
<p>metona-sqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">235+</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">~10KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">4</div><div class="label">存储引擎</div></div>
</div>
</div>
</section>
<footer>
<div class="container">
<p>© 2026 <a href="https://git.metona.cn/MetonaTeam/metona-sqlark">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>
+202
View File
@@ -0,0 +1,202 @@
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid'];
/** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.1.12';
+273
View File
@@ -0,0 +1,273 @@
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parse } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
t = new Table(this.engine, name, this.executor);
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
await this.pluginManager.trigger('beforeQuery', sql);
const stmt: Statement = parse(sql);
const result = await this.executor.execute(stmt);
await this.pluginManager.trigger('afterQuery', sql, result);
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
return this.engine.insert(tableName, data);
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark Engine — 存储引擎层
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
+185
View File
@@ -0,0 +1,185 @@
/**
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
* @module engine/indexeddb
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { MemoryEngine } from './memory';
import { matchWhere, applyOrderBy, projectColumns } from '../query/where-matcher';
export class IndexedDBEngine implements IStorageEngine {
readonly name = 'indexeddb';
private db: IDBDatabase | null = null;
private dbName = '';
private version = 1;
private memoryCache: MemoryEngine = new MemoryEngine();
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName; this.version = version;
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
async close(): Promise<void> {
if (this.db) { this.db.close(); this.db = null; }
await this.memoryCache.close();
}
isOpen(): boolean { return this.db !== null; }
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
const store = db.createObjectStore(schema.name, { keyPath: pkColumn });
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index && colName !== pkColumn) {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false });
}
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(tableName)) db.deleteObjectStore(tableName);
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async hasTable(tableName: string): Promise<boolean> { return this.ensureDB().objectStoreNames.contains(tableName); }
async getTableNames(): Promise<string[]> { return Array.from(this.ensureDB().objectStoreNames); }
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.memoryCache.getTableSchema(tableName); }
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
await this.memoryCache.insert(tableName, rows);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const pks: string[] = [];
for (const row of rows) {
const req = store.add(row);
req.onsuccess = () => pks.push(String(req.result));
req.onerror = () => {}; // 内存层已校验,忽略 IDB 重复键
}
tx.oncomplete = () => resolve(pks);
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const req = tx.objectStore(tableName).getAll();
req.onsuccess = () => {
let results: Record<string, unknown>[] = req.result ?? [];
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where!));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((r) => projectColumns(r, query.columns!));
}
resolve(results);
};
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
});
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
await this.memoryCache.update(tableName, query, updates);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const getAllReq = store.getAll();
let count = 0;
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
Object.assign(row, updates); store.put(row); count++;
}
}
};
tx.oncomplete = () => resolve(count);
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
await this.memoryCache.delete(tableName, query);
const db = this.ensureDB();
const schema = await this.memoryCache.getTableSchema(tableName);
if (!schema) throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const getAllReq = store.getAll();
let count = 0;
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
store.delete(row[pkColumn] as IDBValidKey); count++;
}
}
};
tx.oncomplete = () => resolve(count);
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
return results.length;
}
async clear(tableName: string): Promise<void> {
await this.memoryCache.clear(tableName);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
tx.objectStore(tableName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
return this.db;
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* metona-sqlark Engine Interface — 存储引擎抽象接口
* @module engine/interface
*/
import type { QueryPlan, TableSchema } from '../constants';
// ---------------------------------------------------------------------------
// IStorageEngine — 所有存储引擎必须实现的接口
// ---------------------------------------------------------------------------
export interface IStorageEngine {
/** 引擎名称 */
readonly name: string;
/** 打开数据库 */
open(dbName: string, version: number): Promise<void>;
/** 关闭数据库 */
close(): Promise<void>;
/** 检查数据库是否已打开 */
isOpen(): boolean;
/** 创建表 */
createTable(schema: TableSchema): Promise<void>;
/** 删除表 */
dropTable(tableName: string): Promise<void>;
/** 检查表是否存在 */
hasTable(tableName: string): Promise<boolean>;
/** 获取所有表名 */
getTableNames(): Promise<string[]>;
/** 获取表结构 */
getTableSchema(tableName: string): Promise<TableSchema | null>;
/** 插入行,返回主键值列表 */
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
/** 查询行 */
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** 更新行,返回影响行数 */
update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number>;
/** 删除行,返回影响行数 */
delete(tableName: string, query: QueryPlan): Promise<number>;
/** 计数 */
count(tableName: string, query?: QueryPlan): Promise<number>;
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
}
+215
View File
@@ -0,0 +1,215 @@
/**
* metona-sqlark Memory Engine — 基于 Map 的内存存储引擎
* @module engine/memory
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { matchWhere, applyOrderBy, projectColumns } from '../query/where-matcher';
export class MemoryEngine implements IStorageEngine {
readonly name = 'memory';
private tables: Map<string, Map<string, Record<string, unknown>>> = new Map();
private schemas: Map<string, TableSchema> = new Map();
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
private opened = false;
// ---- 生命周期 ----
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
async close(): Promise<void> {
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.opened = false;
}
isOpen(): boolean { return this.opened; }
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
if (this.schemas.has(schema.name)) throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
this.schemas.set(schema.name, schema);
this.tables.set(schema.name, new Map());
const tableIndexes = new Map<string, Map<unknown, Set<string>>>();
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index || colDef.unique) tableIndexes.set(colName, new Map());
}
this.indexes.set(schema.name, tableIndexes);
}
async dropTable(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.schemas.delete(tableName); this.tables.delete(tableName); this.indexes.delete(tableName);
}
async hasTable(tableName: string): Promise<boolean> { return this.schemas.has(tableName); }
async getTableNames(): Promise<string[]> { return Array.from(this.schemas.keys()); }
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.schemas.get(tableName) ?? null; }
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
const pkColumn = this.getPrimaryKey(schema);
const pks: string[] = [];
for (const row of rows) {
const validatedRow = this.validateRow(schema, row);
const pkValue = String(validatedRow[pkColumn]);
if (table.has(pkValue)) throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
this.checkUniqueness(schema, validatedRow);
table.set(pkValue, validatedRow);
this.updateIndexes(tableName, validatedRow, pkValue);
pks.push(pkValue);
}
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
let results = this.tryIndexLookup(tableName, table, query);
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where!));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((row) => projectColumns(row, query.columns!));
}
return results;
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
let count = 0;
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
const updated = { ...row, ...updates };
this.validateRow(schema, updated);
table.set(pk, updated);
count++;
}
}
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
const toDelete: string[] = [];
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
toDelete.push(pk);
}
}
for (const pk of toDelete) table.delete(pk);
return toDelete.length;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
if (!query?.where || Object.keys(query.where).length === 0) return table.size;
let result = 0;
for (const row of table.values()) { if (matchWhere(row, query.where)) result++; }
return result;
}
async clear(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.tables.get(tableName)!.clear();
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
}
// ---- 内部辅助 ----
private ensureTable(tableName: string): void {
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
}
private getPrimaryKey(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) { if (col.primaryKey) return name; }
return Object.keys(schema.columns)[0];
}
private validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
const validated: Record<string, unknown> = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
if (value === undefined && colDef.default !== undefined) value = colDef.default;
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
}
if (value !== undefined && value !== null) this.checkType(colName, colDef.type, value);
if (value !== undefined) validated[colName] = value;
}
return validated;
}
private checkType(colName: string, type: string, value: unknown): void {
const jsType = typeof value;
switch (type) {
case 'string': if (jsType !== 'string') throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR'); break;
case 'number': if (jsType !== 'number') throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR'); break;
case 'boolean': if (jsType !== 'boolean') throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR'); break;
case 'date': if (jsType !== 'string' || isNaN(Date.parse(value as string))) throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR'); break;
case 'json': if (jsType !== 'object') throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR'); break;
}
}
/** O(1) 唯一性检查:利用哈希索引 */
private checkUniqueness(schema: TableSchema, row: Record<string, unknown>): void {
const tableIndexes = this.indexes.get(schema.name);
if (!tableIndexes) return;
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.unique || row[colName] === undefined || row[colName] === null) continue;
const colIndex = tableIndexes.get(colName);
if (colIndex && colIndex.has(row[colName])) {
throw new DatabaseError(`Unique constraint violation on column "${colName}" in table "${schema.name}"`, 'UNIQUE_VIOLATION');
}
}
}
/** 索引查找 */
private tryIndexLookup(
tableName: string, table: Map<string, Record<string, unknown>>, query: QueryPlan,
): Record<string, unknown>[] {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes || !query.where) return Array.from(table.values());
for (const [col, condition] of Object.entries(query.where)) {
if (typeof condition !== 'object' || condition === null) {
const colIndex = tableIndexes.get(col);
if (colIndex) {
const pks = colIndex.get(condition);
if (pks) {
const result: Record<string, unknown>[] = [];
for (const pk of pks) { const r = table.get(pk); if (r) result.push(r); }
return result;
}
return [];
}
}
}
return Array.from(table.values());
}
/** 更新索引 */
private updateIndexes(tableName: string, row: Record<string, unknown>, pk: string): void {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes) return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
}
}
+174
View File
@@ -0,0 +1,174 @@
/**
* metona-sqlark OPFS Engine — 基于 Origin Private File System 的持久化存储引擎
* @module engine/opfs
*
* 使用 JSON-per-table 文件存储方案。
* 目录结构:{dbName}/tables/{tableName}.json
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { MemoryEngine } from './memory';
// ---------------------------------------------------------------------------
// OPFSEngine
// ---------------------------------------------------------------------------
export class OPFSEngine implements IStorageEngine {
readonly name = 'opfs';
private root: FileSystemDirectoryHandle | null = null;
private tablesDir: FileSystemDirectoryHandle | null = null;
private dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟)
private memoryCache: MemoryEngine = new MemoryEngine();
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName;
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
}
async close(): Promise<void> {
this.root = null;
this.tablesDir = null;
await this.memoryCache.close();
}
isOpen(): boolean {
return this.tablesDir !== null;
}
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
// OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
if (this.tablesDir) {
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
} catch {
// 文件不存在则忽略
}
}
}
async hasTable(tableName: string): Promise<boolean> {
if (!this.tablesDir) return false;
try {
await this.tablesDir.getFileHandle(`${tableName}.json`);
return true;
} catch {
return false;
}
}
async getTableNames(): Promise<string[]> {
if (!this.tablesDir) return [];
const names: string[] = [];
for await (const [name] of (this.tablesDir as any).entries()) {
if (name.endsWith('.json')) {
names.push(name.replace('.json', ''));
}
}
return names;
}
async getTableSchema(tableName: string): Promise<TableSchema | null> {
return this.memoryCache.getTableSchema(tableName);
}
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
const pks = await this.memoryCache.insert(tableName, rows);
// 持久化到 OPFS
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
return this.memoryCache.find(tableName, query);
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryCache.update(tableName, query, updates);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryCache.delete(tableName, query);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
return this.memoryCache.count(tableName, query);
}
async clear(tableName: string): Promise<void> {
await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
}
// ---- 内部辅助 ----
private ensureDir(): FileSystemDirectoryHandle {
if (!this.tablesDir) {
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
}
return this.tablesDir;
}
private async writeTableData(tableName: string, data: Record<string, unknown>[]): Promise<void> {
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data));
await writable.close();
}
private async readTableData(tableName: string): Promise<Record<string, unknown>[]> {
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
try {
const fileHandle = await dir.getFileHandle(fileName);
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
} catch {
return [];
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName: string, schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
const rows = await this.readTableData(tableName);
if (rows.length > 0) {
// 直接用 Map 设置绕过 insert 校验
for (const row of rows) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* metona-sqlark Hybrid Engine — 内存 + 磁盘混合存储引擎
* @module hybrid/index
*
* 采用 write-through 策略:
* - 所有写操作同时写入内存和磁盘
* - 所有读操作直接从内存返回
* - 数据库打开时从磁盘加载数据到内存
*/
import type { IStorageEngine } from '../engine/interface';
import type { QueryPlan, TableSchema, DiskEngine } from '../constants';
import { MemoryEngine } from '../engine/memory';
import { IndexedDBEngine } from '../engine/indexeddb';
import { OPFSEngine } from '../engine/opfs';
// ---------------------------------------------------------------------------
// HybridEngine
// ---------------------------------------------------------------------------
export class HybridEngine implements IStorageEngine {
readonly name = 'hybrid';
private memoryEngine: MemoryEngine;
private diskEngine: IStorageEngine;
private diskEngineType: DiskEngine;
constructor(diskEngine: DiskEngine = 'indexeddb') {
this.memoryEngine = new MemoryEngine();
this.diskEngineType = diskEngine;
this.diskEngine = diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
}
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
// 先打开磁盘引擎
await this.diskEngine.open(dbName, version);
// 再打开内存引擎
await this.memoryEngine.open(dbName, version);
// 从磁盘加载现存表
const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName);
if (!schema) continue;
// 在内存中创建表
await this.memoryEngine.createTable(schema);
// 从磁盘加载数据到内存
const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) {
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
}
}
}
async close(): Promise<void> {
await this.memoryEngine.close();
await this.diskEngine.close();
}
isOpen(): boolean {
return this.memoryEngine.isOpen() && this.diskEngine.isOpen();
}
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryEngine.createTable(schema);
await this.diskEngine.createTable(schema);
}
async dropTable(tableName: string): Promise<void> {
await this.memoryEngine.dropTable(tableName);
await this.diskEngine.dropTable(tableName);
}
async hasTable(tableName: string): Promise<boolean> {
return this.memoryEngine.hasTable(tableName);
}
async getTableNames(): Promise<string[]> {
return this.memoryEngine.getTableNames();
}
async getTableSchema(tableName: string): Promise<TableSchema | null> {
return this.memoryEngine.getTableSchema(tableName);
}
// ---- CRUDwrite-through 策略) ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
const pks = await this.memoryEngine.insert(tableName, rows);
// write-through: 同步写入磁盘
await this.diskEngine.insert(tableName, rows);
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
// 直接从内存读取
return this.memoryEngine.find(tableName, query);
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryEngine.update(tableName, query, updates);
// write-through: 同步更新磁盘
await this.diskEngine.update(tableName, query, updates);
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryEngine.delete(tableName, query);
// write-through: 同步删除磁盘
await this.diskEngine.delete(tableName, query);
return count;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
return this.memoryEngine.count(tableName, query);
}
async clear(tableName: string): Promise<void> {
await this.memoryEngine.clear(tableName);
await this.diskEngine.clear(tableName);
}
// ---- 引擎信息 ----
/** 获取磁盘引擎类型 */
getDiskEngineType(): DiskEngine {
return this.diskEngineType;
}
/** 获取内存引擎(供内部使用) */
getMemoryEngine(): MemoryEngine {
return this.memoryEngine;
}
}
+83
View File
@@ -0,0 +1,83 @@
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.1.12
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
* 创建数据库实例并初始化
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse } from './sql/parser';
export { tokenize } from './sql/lexer';
+80
View File
@@ -0,0 +1,80 @@
// @ts-nocheck
/**
* metona-sqlark React Integration (v0.1.11)
* @module integrations/react
*
* 轻量 React hooks,需要 react 作为 peer dependency。
*
* @example
* import { useQuery, useTable } from 'metona-sqlark/react';
* import { db } from './db';
*
* function UserList() {
* const { data, loading, refresh } = useQuery(db, 'SELECT * FROM users');
* return loading ? <div>Loading...</div> : <div>{data.map(u => <div key={u.id}>{u.name}</div>)}</div>;
* }
*/
import type { MetonaSqlark } from '../core';
import { useState, useEffect, useCallback, useRef } from 'react';
/** useQuery: 执行 SQL 查询 */
export function useQuery(
db: MetonaSqlark,
sql: string,
deps: unknown[] = [],
): { data: Record<string, unknown>[]; loading: boolean; error: Error | null; refresh: () => void } {
const [data, setData] = useState<Record<string, unknown>[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
try {
const result = await db.query(sql) as Record<string, unknown>[];
setData(result);
setError(null);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [db, sql, ...deps]);
useEffect(() => { execute(); }, [execute]);
return { data, loading, error, refresh: execute };
}
/** useTable: 快速获取表数据 */
export function useTable(
db: MetonaSqlark,
tableName: string,
): { data: Record<string, unknown>[]; loading: boolean; refresh: () => void } {
const { data, loading, refresh } = useQuery(db, `SELECT * FROM ${tableName}`, [tableName]);
return { data, loading, refresh };
}
/** useDatabase: 创建/管理数据库实例 */
export function useDatabase(config: import('../constants').DatabaseConfig): {
db: MetonaSqlark | null;
ready: boolean;
error: Error | null;
} {
const [db, setDb] = useState<MetonaSqlark | null>(null);
const [ready, setReady] = useState(false);
const [error, setError] = useState<Error | null>(null);
const initRef = useRef(false);
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
const instance = new MetonaSqlark(config);
instance.init()
.then(() => { setDb(instance); setReady(true); })
.catch(setError);
return () => { instance.close(); };
}, []);
return { db, ready, error };
}
+77
View File
@@ -0,0 +1,77 @@
// @ts-nocheck
/**
* metona-sqlark Vue Integration (v0.1.11)
* @module integrations/vue
*
* 轻量 Vue composables,需要 vue 作为 peer dependency。
*
* @example
* import { useSqlarkQuery, useSqlarkTable } from 'metona-sqlark/vue';
* import { db } from './db';
*
* const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
*/
import type { MetonaSqlark } from '../core';
import { ref, watch, onMounted, type Ref } from 'vue';
/** useSqlarkQuery: 执行 SQL 查询 */
export function useSqlarkQuery(
db: MetonaSqlark,
sql: string,
deps: Ref<unknown>[] = [],
): { data: Ref<Record<string, unknown>[]>; loading: Ref<boolean>; error: Ref<Error | null>; refresh: () => void } {
const data = ref<Record<string, unknown>[]>([]) as Ref<Record<string, unknown>[]>;
const loading = ref(true);
const error = ref<Error | null>(null) as Ref<Error | null>;
const execute = async () => {
loading.value = true;
try {
data.value = await db.query(sql) as Record<string, unknown>[];
error.value = null;
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
onMounted(execute);
watch([() => sql, ...deps], execute);
return { data, loading, error, refresh: execute };
}
/** useSqlarkTable: 快速获取表数据 */
export function useSqlarkTable(
db: MetonaSqlark,
tableName: string,
): { data: Ref<Record<string, unknown>[]>; loading: Ref<boolean>; refresh: () => void } {
const { data, loading, refresh } = useSqlarkQuery(db, `SELECT * FROM ${tableName}`);
return { data, loading, refresh };
}
/** useSqlarkDatabase: 创建/管理数据库实例 */
export function useSqlarkDatabase(config: import('../constants').DatabaseConfig): {
db: Ref<MetonaSqlark | null>;
ready: Ref<boolean>;
error: Ref<Error | null>;
} {
const db = ref<MetonaSqlark | null>(null) as Ref<MetonaSqlark | null>;
const ready = ref(false);
const error = ref<Error | null>(null) as Ref<Error | null>;
onMounted(async () => {
try {
const instance = new MetonaSqlark(config);
await instance.init();
db.value = instance;
ready.value = true;
} catch (e) {
error.value = e as Error;
}
});
return { db, ready, error };
}
+91
View File
@@ -0,0 +1,91 @@
/**
* metona-sqlark Plugin — 插件系统
* @module plugin
*
* 管理插件的注册、生命周期和钩子调度。
*/
import type { MetonaPlugin, HookName } from '../constants';
// ---------------------------------------------------------------------------
// Hook 回调类型
// ---------------------------------------------------------------------------
export type HookCallback = (...args: unknown[]) => void | Promise<void>;
// ---------------------------------------------------------------------------
// PluginManager
// ---------------------------------------------------------------------------
export class PluginManager {
private plugins: MetonaPlugin[] = [];
private hooks: Map<HookName, HookCallback[]> = new Map();
/** 注册插件 */
register(plugin: MetonaPlugin): void {
// 按优先级插入
const priority = plugin.priority ?? 0;
const insertIndex = this.plugins.findIndex(
(p) => (p.priority ?? 0) < priority,
);
if (insertIndex === -1) {
this.plugins.push(plugin);
} else {
this.plugins.splice(insertIndex, 0, plugin);
}
// 安装
plugin.install(null); // 实际引用由 MetonaSqlark 注入
}
/** 卸载插件 */
unregister(pluginName: string): void {
const idx = this.plugins.findIndex((p) => p.name === pluginName);
if (idx !== -1) {
this.plugins[idx].destroy();
this.plugins.splice(idx, 1);
}
}
/** 获取所有已注册插件 */
getPlugins(): MetonaPlugin[] {
return [...this.plugins];
}
/** 添加钩子回调 */
on(hook: HookName, callback: HookCallback): void {
const callbacks = this.hooks.get(hook) ?? [];
callbacks.push(callback);
this.hooks.set(hook, callbacks);
}
/** 移除钩子回调 */
off(hook: HookName, callback: HookCallback): void {
const callbacks = this.hooks.get(hook);
if (callbacks) {
const idx = callbacks.indexOf(callback);
if (idx !== -1) callbacks.splice(idx, 1);
}
}
/** 触发钩子 */
async trigger(hook: HookName, ...args: unknown[]): Promise<void> {
const callbacks = this.hooks.get(hook);
if (callbacks) {
for (const cb of callbacks) {
await cb(...args);
}
}
}
/** 销毁所有插件 */
destroy(): void {
for (const plugin of this.plugins) {
try { plugin.destroy(); } catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Plugin "${plugin.name}" destroy error:`, e);
}
}
this.plugins = [];
this.hooks.clear();
}
}
+157
View File
@@ -0,0 +1,157 @@
/**
* metona-sqlark Query AST — 查询抽象语法树类型定义
* @module query/ast
*
* QueryBuilder 和 SQL Parser 统一输出此 AST
* Executor 只认 AST,保证两种查询接口行为一致。
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
on: WhereCondition;
}
// ---------------------------------------------------------------------------
// 聚合函数
// ---------------------------------------------------------------------------
/** 聚合函数类型 */
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
/** 聚合表达式 */
export interface AggregateExpression {
type: 'AGGREGATE';
func: AggregateFunc;
column: string; // '*' for COUNT(*)
alias?: string;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
values: unknown[][];
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement;
+182
View File
@@ -0,0 +1,182 @@
/**
* metona-sqlark Query Builder — 链式查询构建器
* @module query/builder
*
* 链式调用 → 构建 AST → 执行引擎操作。
* 支持 JOIN(需要 Executor)。
*/
import type { IStorageEngine } from '../engine/interface';
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
import type { SelectStatement, UpdateStatement, DeleteStatement, JoinType, JoinClause } from './ast';
import { QueryExecutor } from './executor';
// ---------------------------------------------------------------------------
// SelectQueryBuilder
// ---------------------------------------------------------------------------
export class SelectQueryBuilder {
private _where: WhereCondition = {};
private _orderBy: OrderBy[] = [];
private _limit?: number;
private _offset?: number;
private _joins: JoinClause[] = [];
private _alias?: string;
private _executor?: QueryExecutor;
constructor(
private engine: IStorageEngine,
private tableName: string,
private _columns: string[] = ['*'],
executor?: QueryExecutor,
) {
this._executor = executor;
}
/** 主表别名 */
as(alias: string): this {
this._alias = alias;
return this;
}
/** INNER JOIN */
innerJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('INNER', table, on, alias);
}
/** LEFT JOIN */
leftJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('LEFT', table, on, alias);
}
/** RIGHT JOIN */
rightJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('RIGHT', table, on, alias);
}
/** CROSS JOIN */
crossJoin(table: string, alias?: string): this {
return this._addJoin('CROSS', table, {}, alias);
}
/** 通用 JOIN */
join(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('INNER', table, on, alias);
}
private _addJoin(type: JoinType, table: string, on: WhereCondition, alias?: string): this {
this._joins.push({ type, table, on, alias });
return this;
}
/** 添加过滤条件 */
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
/** 排序 */
orderBy(column: string, direction: SortDirection = 'asc'): this {
this._orderBy.push({ column, direction });
return this;
}
/** 限制返回条数 */
limit(n: number): this {
this._limit = n;
return this;
}
/** 偏移量 */
offset(n: number): this {
this._offset = n;
return this;
}
/** 执行查询 */
async execute(): Promise<Record<string, unknown>[]> {
// 有 JOIN → 通过 Executor 执行
if (this._joins.length > 0 && this._executor) {
const ast = this.toAST();
return this._executor.execute(ast) as Promise<Record<string, unknown>[]>;
}
// 无 JOIN → 直接调用引擎
return this.engine.find(this.tableName, {
table: this.tableName,
columns: this._columns,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
});
}
/** 获取 AST */
toAST(): SelectStatement {
return {
type: 'SELECT',
columns: this._columns,
from: this.tableName,
alias: this._alias,
joins: this._joins.length > 0 ? [...this._joins] : undefined,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
};
}
}
// ---------------------------------------------------------------------------
// UpdateQueryBuilder
// ---------------------------------------------------------------------------
export class UpdateQueryBuilder {
private _where: WhereCondition = {};
constructor(
private engine: IStorageEngine,
private tableName: string,
private _updates: Record<string, unknown>,
) {}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
async execute(): Promise<number> {
return this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
}
toAST(): UpdateStatement {
return { type: 'UPDATE', table: this.tableName, sets: this._updates, where: this._where };
}
}
// ---------------------------------------------------------------------------
// DeleteQueryBuilder
// ---------------------------------------------------------------------------
export class DeleteQueryBuilder {
private _where: WhereCondition = {};
constructor(
private engine: IStorageEngine,
private tableName: string,
) {}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
async execute(): Promise<number> {
return this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
}
toAST(): DeleteStatement {
return { type: 'DELETE', from: this.tableName, where: this._where };
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* metona-sqlark Query Compiler — AST → 查询计划
* @module query/compiler
*
* 将 AST 语句编译为引擎可执行的 QueryPlan。
* v0.0.1: 简单直接映射,未来可加入索引选择、过滤下推等优化。
*/
import type { QueryPlan } from '../constants';
import { DatabaseError } from '../constants';
import type { Statement, SelectStatement, DeleteStatement, UpdateStatement } from './ast';
// ---------------------------------------------------------------------------
// 编译 AST → QueryPlan
// ---------------------------------------------------------------------------
/**
* 编译 SELECT / DELETE / UPDATE 语句为 QueryPlan。
* INSERT 和 DDL 语句不需要 QueryPlan。
*/
export function compileStatement(stmt: Statement): QueryPlan {
switch (stmt.type) {
case 'SELECT':
return compileSelect(stmt);
case 'DELETE':
return compileDelete(stmt);
case 'UPDATE':
return compileUpdate(stmt);
default:
throw new DatabaseError(`Cannot compile statement type "${stmt.type}" to QueryPlan`, 'COMPILE_ERROR');
}
}
function compileSelect(stmt: SelectStatement): QueryPlan {
return {
table: stmt.from,
columns: stmt.columns,
where: stmt.where,
orderBy: stmt.orderBy?.length ? stmt.orderBy : undefined,
limit: stmt.limit,
offset: stmt.offset,
};
}
function compileDelete(stmt: DeleteStatement): QueryPlan {
return {
table: stmt.from,
where: stmt.where,
};
}
function compileUpdate(stmt: UpdateStatement): QueryPlan {
return {
table: stmt.table,
where: stmt.where,
};
}
+229
View File
@@ -0,0 +1,229 @@
/**
* metona-sqlark Query Executor — AST 执行器
* @module query/executor
*
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
*/
import type { IStorageEngine } from '../engine/interface';
import type {
Statement, SelectStatement, InsertStatement, UpdateStatement,
DeleteStatement, CreateTableStatement, DropTableStatement, JoinClause,
} from './ast';
import { DatabaseError } from '../constants';
import { compileStatement } from './compiler';
import { createSchema, astColumnToColumnDef } from '../table/schema';
import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
// ---------------------------------------------------------------------------
// Executor
// ---------------------------------------------------------------------------
export class QueryExecutor {
constructor(private engine: IStorageEngine) {}
async execute(stmt: Statement): Promise<unknown> {
switch (stmt.type) {
case 'SELECT': return this.executeSelect(stmt);
case 'INSERT': return this.executeInsert(stmt);
case 'UPDATE': return this.executeUpdate(stmt);
case 'DELETE': return this.executeDelete(stmt);
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
case 'DROP_TABLE': return this.executeDropTable(stmt);
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
}
}
// ===================================================================
// SELECT
// ===================================================================
private async executeSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
let rows: Record<string, unknown>[];
if (!stmt.joins || stmt.joins.length === 0) {
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
rows = await this.engine.find(plan.table, plan);
} else {
rows = await this.executeJoinSelect(stmt);
}
if (hasGroupBy) rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct) rows = this.executeDistinct(rows);
if (stmt.having && Object.keys(stmt.having).length > 0) {
rows = rows.filter((row) => matchWhere(row, stmt.having!));
}
if (stmt.orderBy && stmt.orderBy.length > 0) rows = applyOrderBy(rows, stmt.orderBy);
const offset = stmt.offset ?? 0;
const limit = stmt.limit ?? rows.length;
rows = rows.slice(offset, offset + limit);
if (!hasGroupBy && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
rows = rows.map((row) => projectColumns(row, stmt.columns));
}
return rows;
}
// ---- JOIN ----
private async executeJoinSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
const mainAlias = stmt.alias ?? stmt.from;
const mainRows = (await this.engine.find(stmt.from, { table: stmt.from }))
.map((row) => this.prefixRow(row, mainAlias));
let resultRows = mainRows;
for (const join of stmt.joins!) {
const joinAlias = join.alias ?? join.table;
const joinRows = (await this.engine.find(join.table, { table: join.table }))
.map((row) => this.prefixRow(row, joinAlias));
resultRows = this.joinRows(resultRows, joinRows, join);
}
if (stmt.where && Object.keys(stmt.where).length > 0) {
resultRows = resultRows.filter((row) => matchWhere(row, stmt.where));
}
return resultRows;
}
private prefixRow(row: Record<string, unknown>, alias: string): Record<string, unknown> {
const prefixed: Record<string, unknown> = {};
for (const [key, value] of Object.entries(row)) prefixed[`${alias}.${key}`] = value;
return prefixed;
}
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
private joinRows(
leftRows: Record<string, unknown>[],
rightRows: Record<string, unknown>[],
join: JoinClause,
): Record<string, unknown>[] {
if (join.type === 'CROSS') {
const result: Record<string, unknown>[] = [];
for (const l of leftRows) for (const r of rightRows) result.push({ ...l, ...r });
return result;
}
const result: Record<string, unknown>[] = [];
for (const l of leftRows) {
let matched = false;
for (const r of rightRows) {
// 合并后匹配 ON(避免创建临时对象再丢弃)
const merged = { ...l, ...r };
if (matchWhere(merged, join.on, { $col: true })) {
result.push(merged);
matched = true;
}
}
if (!matched && join.type === 'LEFT') {
const nullRight: Record<string, unknown> = {};
for (const key of Object.keys(rightRows[0] ?? {})) nullRight[key] = null;
result.push({ ...l, ...nullRight });
}
}
if (join.type === 'RIGHT') {
for (const r of rightRows) {
const isMatched = leftRows.some((l) => {
const merged = { ...l, ...r };
return matchWhere(merged, join.on, { $col: true });
});
if (!isMatched) {
const nullLeft: Record<string, unknown> = {};
for (const key of Object.keys(leftRows[0] ?? {})) nullLeft[key] = null;
result.push({ ...nullLeft, ...r });
}
}
}
return result;
}
// ---- GROUP BY ----
private executeGroupBy(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown>[] {
const groups = new Map<string, Record<string, unknown>[]>();
for (const row of rows) {
const key = stmt.groupBy!.map((col) => String(row[col] ?? 'null')).join('|');
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(row);
}
const result: Record<string, unknown>[] = [];
for (const groupRows of groups.values()) {
const aggregated: Record<string, unknown> = {};
for (const col of stmt.groupBy!) aggregated[col] = groupRows[0][col];
for (const colExpr of stmt.columns) {
if (colExpr === '*') continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
aggregated[alias || colExpr] = this.computeAggregate(func.toUpperCase(), groupRows, arg.trim());
} else if (!stmt.groupBy!.includes(colExpr)) {
aggregated[colExpr] = groupRows[0][colExpr];
}
}
result.push(aggregated);
}
return result;
}
private computeAggregate(func: string, rows: Record<string, unknown>[], col: string): number {
const nums = rows.map((r) => r[col]).filter((v) => v !== null && v !== undefined).map(Number);
switch (func) {
case 'COUNT': return col === '*' ? rows.length : nums.length;
case 'SUM': return nums.reduce((a: number, b) => a + b, 0);
case 'AVG': return nums.length === 0 ? 0 : nums.reduce((a: number, b) => a + b, 0) / nums.length;
case 'MIN': return nums.length === 0 ? 0 : Math.min(...nums);
case 'MAX': return nums.length === 0 ? 0 : Math.max(...nums);
default: return 0;
}
}
// ---- DISTINCT(优化:列值拼接代替 JSON.stringify ----
private executeDistinct(rows: Record<string, unknown>[]): Record<string, unknown>[] {
const seen = new Set<string>();
return rows.filter((row) => {
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// ===================================================================
// 其他语句
// ===================================================================
private async executeInsert(stmt: InsertStatement): Promise<string[]> {
const schema = await this.engine.getTableSchema(stmt.into);
if (!schema) throw new DatabaseError(`Table "${stmt.into}" does not exist`, 'TABLE_NOT_FOUND');
const colNames = stmt.columns ?? Object.keys(schema.columns);
const rows: Record<string, unknown>[] = stmt.values.map((vals: unknown[]) => {
const row: Record<string, unknown> = {};
for (let i = 0; i < colNames.length; i++) { if (i < vals.length) row[colNames[i]] = vals[i]; }
return row;
});
return this.engine.insert(stmt.into, rows);
}
private async executeUpdate(stmt: UpdateStatement): Promise<number> {
const plan = compileStatement(stmt);
return this.engine.update(plan.table, plan, stmt.sets);
}
private async executeDelete(stmt: DeleteStatement): Promise<number> {
const plan = compileStatement(stmt);
return this.engine.delete(plan.table, plan);
}
private async executeCreateTable(stmt: CreateTableStatement): Promise<void> {
const columns: Record<string, any> = {};
for (const col of stmt.columns) columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns));
}
private async executeDropTable(stmt: DropTableStatement): Promise<void> {
return this.engine.dropTable(stmt.name);
}
getEngine(): IStorageEngine { return this.engine; }
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark Query — 查询系统
* @module query
*/
export type * from './ast';
export { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from './builder';
export { compileStatement } from './compiler';
export { QueryExecutor } from './executor';
+173
View File
@@ -0,0 +1,173 @@
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
* @module query/where-matcher
*
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// LIKE 正则缓存
// ---------------------------------------------------------------------------
const likeCache = new Map<string, RegExp>();
function compileLikeRegex(pattern: string): RegExp {
const cached = likeCache.get(pattern);
if (cached) return cached;
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/%/g, '.*')
.replace(/_/g, '.');
const regex = new RegExp(`^${escaped}$`, 'i');
likeCache.set(pattern, regex);
return regex;
}
// ---------------------------------------------------------------------------
// WHERE 匹配(顶层入口)
// ---------------------------------------------------------------------------
/**
* 匹配完整 WHERE 条件
* @param row 当前数据行
* @param where WHERE 条件对象
* @param options.$col 是否启用 $col 列引用解析
*/
export function matchWhere(
row: Record<string, unknown>,
where: WhereCondition,
options: { $col?: boolean } = {},
): boolean {
for (const [field, condition] of Object.entries(where)) {
// 顶层 $and
if (field === '$and') {
const subs = condition as WhereCondition[];
if (!subs.every((sub) => matchWhere(row, sub, options))) return false;
continue;
}
// 顶层 $or
if (field === '$or') {
const subs = condition as WhereCondition[];
if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
continue;
}
if (!matchField(row[field], condition, row, options)) return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 字段匹配
// ---------------------------------------------------------------------------
function matchField(
value: unknown,
condition: unknown,
row: Record<string, unknown>,
options: { $col?: boolean },
): boolean {
// 嵌套 $and
if (typeof condition === 'object' && condition !== null && '$and' in (condition as Record<string, unknown>)) {
const subs = (condition as Record<string, unknown>).$and as WhereCondition[];
return subs.every((sub) => matchWhere(row, sub, options));
}
// 嵌套 $or
if (typeof condition === 'object' && condition !== null && '$or' in (condition as Record<string, unknown>)) {
const subs = (condition as Record<string, unknown>).$or as WhereCondition[];
return subs.some((sub) => matchWhere(row, sub, options));
}
// $not
if (typeof condition === 'object' && condition !== null && '$not' in (condition as Record<string, unknown>)) {
return !matchField(value, (condition as Record<string, unknown>).$not, row, options);
}
// 简单值 => $eq
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
return value === condition;
}
const ops = condition as Record<string, unknown>;
// $col 简写: { $col: name } === { $eq: { $col: name } }(仅 JOIN ON 场景)
if (options.$col && '$col' in ops && Object.keys(ops).length === 1) {
return value === row[ops.$col as string];
}
// 遍历操作符
for (const [op, operand] of Object.entries(ops)) {
let actualOperand = operand;
// $col 列引用解析
if (options.$col && typeof operand === 'object' && operand !== null && '$col' in (operand as Record<string, unknown>)) {
actualOperand = row[(operand as Record<string, unknown>).$col as string];
}
if (!matchOperator(value, op, actualOperand)) return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 操作符匹配
// ---------------------------------------------------------------------------
function matchOperator(value: unknown, op: string, operand: unknown): boolean {
switch (op) {
case '$eq': return value === operand;
case '$ne': return value !== operand;
case '$gt': return (value as number) > (operand as number);
case '$gte': return (value as number) >= (operand as number);
case '$lt': return (value as number) < (operand as number);
case '$lte': return (value as number) <= (operand as number);
case '$in': return Array.isArray(operand) && operand.includes(value);
case '$nin': return Array.isArray(operand) && !operand.includes(value);
case '$like': return compileLikeRegex(String(operand)).test(String(value));
default: return true;
}
}
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
export function applyOrderBy(rows: Record<string, unknown>[], orderBy: OrderBy[]): Record<string, unknown>[] {
return [...rows].sort((a, b) => {
for (const { column, direction } of orderBy) {
const cmp = compare(a[column], b[column]);
if (cmp !== 0) return direction === 'desc' ? -cmp : cmp;
}
return 0;
});
}
function compare(a: unknown, b: unknown): number {
if (a === b) return 0;
if (a === null || a === undefined) return 1;
if (b === null || b === undefined) return -1;
if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
if (typeof a === 'number' && typeof b === 'number') return a - b;
return String(a).localeCompare(String(b));
}
// ---------------------------------------------------------------------------
// 列投影
// ---------------------------------------------------------------------------
export function projectColumns(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
const projected: Record<string, unknown> = {};
for (const col of columns) {
if (col in row) {
projected[col] = row[col];
} else {
for (const key of Object.keys(row)) {
if (key.endsWith(`.${col}`) || key === col) {
projected[col] = row[key];
break;
}
}
}
}
return projected;
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark SQL Parser — SQL 字符串解析
* @module sql
*/
export { Lexer, tokenize } from './lexer';
export { Parser, parse } from './parser';
export { TokenType } from './tokens';
export type { Token } from './tokens';
+215
View File
@@ -0,0 +1,215 @@
/**
* metona-sqlark SQL Lexer — 词法分析器
* @module sql/lexer
*
* 将 SQL 字符串切分为 Token 流。
*/
import { TokenType, type Token, KEYWORDS } from './tokens';
// ---------------------------------------------------------------------------
// Lexer
// ---------------------------------------------------------------------------
export class Lexer {
private input: string;
private position = 0;
private readPosition = 0;
private ch: string = '';
constructor(input: string) {
this.input = input;
this.readChar();
}
/** 读取下一个 Token */
nextToken(): Token {
this.skipWhitespace();
let tok: Token;
switch (this.ch) {
case ',':
tok = this.makeToken(TokenType.COMMA, ',');
break;
case '(':
tok = this.makeToken(TokenType.LPAREN, '(');
break;
case ')':
tok = this.makeToken(TokenType.RPAREN, ')');
break;
case ';':
tok = this.makeToken(TokenType.SEMICOLON, ';');
break;
case '*':
tok = this.makeToken(TokenType.STAR, '*');
break;
case '.':
tok = this.makeToken(TokenType.DOT, '.');
break;
case '=':
tok = this.makeToken(TokenType.EQ, '=');
break;
case '!':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '!=');
} else {
tok = this.makeToken(TokenType.ILLEGAL, '!');
}
break;
case '>':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.GTE, '>=');
} else {
tok = this.makeToken(TokenType.GT, '>');
}
break;
case '<':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.LTE, '<=');
} else if (this.peekChar() === '>') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '<>');
} else {
tok = this.makeToken(TokenType.LT, '<');
}
break;
case "'":
case '"':
tok = this.readString(this.ch);
break;
case '':
tok = { type: TokenType.EOF, value: '', position: this.position };
break;
default:
if (this.isLetter(this.ch)) {
const ident = this.readIdentifier();
const keyword = KEYWORDS[ident.toUpperCase()];
tok = {
type: keyword ?? TokenType.IDENTIFIER,
value: ident,
position: this.position - ident.length,
};
return tok; // 已读取完毕,不需要再 readChar
} else if (this.isDigit(this.ch) || (this.ch === '-' && this.isDigit(this.peekChar()))) {
const num = this.readNumber();
tok = {
type: TokenType.NUMBER,
value: num,
position: this.position - num.length,
};
return tok;
} else {
tok = this.makeToken(TokenType.ILLEGAL, this.ch);
}
break;
}
this.readChar();
return tok;
}
// ---- 内部 ----
private readChar(): void {
if (this.readPosition >= this.input.length) {
this.ch = '';
} else {
this.ch = this.input[this.readPosition];
}
this.position = this.readPosition;
this.readPosition++;
}
private peekChar(): string {
if (this.readPosition >= this.input.length) return '';
return this.input[this.readPosition];
}
private skipWhitespace(): void {
while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n' || this.ch === '\r') {
this.readChar();
}
}
private readIdentifier(): string {
const start = this.position;
while (this.isLetter(this.ch) || this.isDigit(this.ch) || this.ch === '_') {
this.readChar();
}
return this.input.slice(start, this.position);
}
private readNumber(): string {
const start = this.position;
// 负号
if (this.ch === '-') this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
// 小数点
if (this.ch === '.' && this.isDigit(this.peekChar())) {
this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
}
return this.input.slice(start, this.position);
}
private readString(quote: string): Token {
const start = this.position + 1; // 跳过一个引号
this.readChar(); // 跳过开始引号
let value = '';
while (this.ch !== quote && this.ch !== '') {
// 处理转义
if (this.ch === '\\' && this.peekChar() === quote) {
this.readChar();
value += quote;
} else {
value += this.ch;
}
this.readChar();
}
// 跳过结束引号(在 readChar 之后才会调用,所以这里不需要处理)
return {
type: TokenType.STRING,
value,
position: start,
};
}
private isLetter(ch: string): boolean {
return /[a-zA-Z_]/.test(ch);
}
private isDigit(ch: string): boolean {
return /[0-9]/.test(ch);
}
private makeToken(type: TokenType, value: string): Token {
return { type, value, position: this.position };
}
}
// ---------------------------------------------------------------------------
// 便捷方法:一次性词法分析
// ---------------------------------------------------------------------------
/** 将 SQL 字符串解析为 Token 列表 */
export function tokenize(sql: string): Token[] {
const lexer = new Lexer(sql);
const tokens: Token[] = [];
let tok = lexer.nextToken();
while (tok.type !== TokenType.EOF) {
tokens.push(tok);
tok = lexer.nextToken();
}
tokens.push(tok); // EOF
return tokens;
}
+717
View File
@@ -0,0 +1,717 @@
/**
* metona-sqlark SQL Parser — 递归下降语法分析器
* @module sql/parser
*
* Token 流 → AST Statement。
* 支持的语法是标准 SQL 的子集。
*/
import { Lexer } from './lexer';
import { TokenType, type Token } from './tokens';
import type {
Statement,
SelectStatement,
InsertStatement,
UpdateStatement,
DeleteStatement,
CreateTableStatement,
DropTableStatement,
ASTColumnDef,
} from '../query/ast';
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
import { DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------
export class Parser {
private lexer: Lexer;
private curToken!: Token;
private peekToken!: Token;
constructor(sql: string) {
this.lexer = new Lexer(sql);
// 预读两个 token
this.nextToken();
this.nextToken();
}
/** 解析完整 SQL 语句 */
parseStatement(): Statement {
switch (this.curToken.type) {
case TokenType.SELECT:
return this.parseSelect();
case TokenType.INSERT:
return this.parseInsert();
case TokenType.UPDATE:
return this.parseUpdate();
case TokenType.DELETE:
return this.parseDelete();
case TokenType.CREATE:
return this.parseCreateTable();
case TokenType.DROP:
return this.parseDropTable();
default:
throw this.error(`Unexpected token "${this.curToken.value}"`);
}
}
// ===================================================================
// SELECT
// ===================================================================
private parseSelect(): SelectStatement {
this.expect(TokenType.SELECT);
// DISTINCT(可选)
let distinct = false;
if (this.curTokenIs(TokenType.DISTINCT)) {
distinct = true;
this.nextToken();
}
// 列
const columns: string[] = [];
if (this.curTokenIs(TokenType.STAR)) {
columns.push('*');
this.nextToken();
} else {
columns.push(...this.parseColumnList());
}
// FROM
this.expect(TokenType.FROM);
const tableName = this.expectIdentifier('table name');
// 表别名(可选)
let alias: string | undefined;
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
alias = this.curToken.value;
this.nextToken();
}
const stmt: SelectStatement = {
type: 'SELECT',
columns,
distinct: distinct || undefined,
from: tableName,
alias,
where: {},
};
// JOIN 子句(可选,支持多个)
const joins = this.parseJoinClauses();
if (joins.length > 0) {
stmt.joins = joins;
}
// WHERE(可选)
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
stmt.where = this.parseCondition();
}
// GROUP BY(可选)
if (this.curTokenIs(TokenType.GROUP)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.groupBy = this.parseIdentifierList();
}
// HAVING(可选)
if (this.curTokenIs(TokenType.HAVING)) {
this.nextToken();
stmt.having = this.parseCondition();
}
// ORDER BY(可选)
if (this.curTokenIs(TokenType.ORDER)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.orderBy = this.parseOrderByList();
}
// LIMIT(可选)
if (this.curTokenIs(TokenType.LIMIT)) {
this.nextToken();
stmt.limit = this.expectNumber('LIMIT value');
}
// OFFSET(可选)
if (this.curTokenIs(TokenType.OFFSET)) {
this.nextToken();
stmt.offset = this.expectNumber('OFFSET value');
}
return stmt;
}
/** 解析 JOIN 子句列表 */
private parseJoinClauses(): import('../query/ast').JoinClause[] {
const joins: import('../query/ast').JoinClause[] = [];
while (this._isJoinKeyword()) {
joins.push(this.parseJoinClause());
}
return joins;
}
private _isJoinKeyword(): boolean {
return (
this.curTokenIs(TokenType.INNER) ||
this.curTokenIs(TokenType.LEFT) ||
this.curTokenIs(TokenType.RIGHT) ||
this.curTokenIs(TokenType.CROSS) ||
this.curTokenIs(TokenType.JOIN)
);
}
/** 解析单个 JOIN 子句 */
private parseJoinClause(): import('../query/ast').JoinClause {
let type: import('../query/ast').JoinType = 'INNER';
if (this.curTokenIs(TokenType.INNER)) {
type = 'INNER';
this.nextToken();
} else if (this.curTokenIs(TokenType.LEFT)) {
type = 'LEFT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER)) this.nextToken(); // 可选 OUTER
} else if (this.curTokenIs(TokenType.RIGHT)) {
type = 'RIGHT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER)) this.nextToken();
} else if (this.curTokenIs(TokenType.CROSS)) {
type = 'CROSS';
this.nextToken();
}
this.expect(TokenType.JOIN);
const tableName = this.expectIdentifier('table name');
// JOIN 表别名(可选)
let alias: string | undefined;
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isJoinReserved()) {
alias = this.curToken.value;
this.nextToken();
}
// ON 条件(CROSS JOIN 不需要 ON
let on = {};
if (type !== 'CROSS' && this.curTokenIs(TokenType.ON)) {
this.nextToken();
on = this.parseCondition();
}
return { type, table: tableName, alias, on };
}
/** 判断当前 token 是否为 FROM 之后的保留字 */
private _isReservedAfterFrom(): boolean {
return (
this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this.curTokenIs(TokenType.OFFSET) ||
this.curTokenIs(TokenType.GROUP) ||
this._isJoinKeyword()
);
}
private _isJoinReserved(): boolean {
return (
this.curTokenIs(TokenType.ON) ||
this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this._isJoinKeyword()
);
}
// ===================================================================
// INSERT
// ===================================================================
private parseInsert(): InsertStatement {
this.expect(TokenType.INSERT);
this.expect(TokenType.INTO);
const tableName = this.expectIdentifier('table name');
// 列名(可选)
let columns: string[] | undefined;
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
columns = this.parseIdentifierList();
this.expect(TokenType.RPAREN);
}
// VALUES
this.expect(TokenType.VALUES);
// 值列表
const values: unknown[][] = [];
do {
if (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
}
this.expect(TokenType.LPAREN);
const rowValues = this.parseValueList();
this.expect(TokenType.RPAREN);
values.push(rowValues);
} while (this.curTokenIs(TokenType.COMMA));
return {
type: 'INSERT',
into: tableName,
columns,
values,
};
}
// ===================================================================
// UPDATE
// ===================================================================
private parseUpdate(): UpdateStatement {
this.expect(TokenType.UPDATE);
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.SET);
// SET col=val, ...
const sets: Record<string, unknown> = {};
do {
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
const col = this.expectIdentifier('column name');
this.expect(TokenType.EQ);
sets[col] = this.parseValue();
} while (this.curTokenIs(TokenType.COMMA));
let where: WhereCondition = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'UPDATE', table: tableName, sets, where };
}
// ===================================================================
// DELETE
// ===================================================================
private parseDelete(): DeleteStatement {
this.expect(TokenType.DELETE);
this.expect(TokenType.FROM);
const tableName = this.expectIdentifier('table name');
let where: WhereCondition = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'DELETE', from: tableName, where };
}
// ===================================================================
// CREATE TABLE
// ===================================================================
private parseCreateTable(): CreateTableStatement {
this.expect(TokenType.CREATE);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.LPAREN);
const columns: ASTColumnDef[] = [];
do {
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
columns.push(this.parseColumnDef());
} while (this.curTokenIs(TokenType.COMMA));
this.expect(TokenType.RPAREN);
return { type: 'CREATE_TABLE', name: tableName, columns };
}
private parseColumnDef(): ASTColumnDef {
const name = this.expectIdentifier('column name');
const type = this.expectIdentifier('column type').toLowerCase();
const col: ASTColumnDef = { name, type };
// 修饰符
while (
this.curTokenIs(TokenType.PRIMARY) ||
this.curTokenIs(TokenType.UNIQUE) ||
this.curTokenIs(TokenType.NOT) ||
this.curTokenIs(TokenType.DEFAULT)
) {
if (this.curTokenIs(TokenType.PRIMARY)) {
this.nextToken();
this.expect(TokenType.KEY);
col.primaryKey = true;
} else if (this.curTokenIs(TokenType.UNIQUE)) {
this.nextToken();
col.unique = true;
} else if (this.curTokenIs(TokenType.NOT)) {
this.nextToken();
// 支持 NOT NULL → required
this.expect(TokenType.NULL);
col.required = true;
} else if (this.curTokenIs(TokenType.DEFAULT)) {
this.nextToken();
col.default = this.parseValue();
} else {
break;
}
}
return col;
}
// ===================================================================
// DROP TABLE
// ===================================================================
private parseDropTable(): DropTableStatement {
this.expect(TokenType.DROP);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName };
}
// ===================================================================
// 条件表达式
// ===================================================================
/** condition → simple_cond ((AND|OR) simple_cond)* */
private parseCondition(): WhereCondition {
let left = this.parseSimpleCondition();
while (this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR)) {
const isAnd = this.curTokenIs(TokenType.AND);
this.nextToken();
const right = this.parseSimpleCondition();
if (isAnd) {
// 合并到 $and
left = { $and: [left, right] } as unknown as WhereCondition;
} else {
left = { $or: [left, right] } as unknown as WhereCondition;
}
}
return left;
}
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
private parseSimpleCondition(): WhereCondition {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner } as unknown as WhereCondition;
}
// (condition)
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
const inner = this.parseCondition();
this.expect(TokenType.RPAREN);
return inner;
}
// column
const column = this.parseColumnRef();
// IS NULL / IS NOT NULL
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'IS') {
this.nextToken();
const isNot = this.curTokenIs(TokenType.NOT);
if (isNot) this.nextToken();
this.expect(TokenType.NULL);
const result: WhereCondition = {};
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
const pattern = this.parseValue();
const result: WhereCondition = {};
result[column] = { $like: pattern };
return result;
}
// IN
if (this.curTokenIs(TokenType.IN)) {
this.nextToken();
this.expect(TokenType.LPAREN);
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result: WhereCondition = {};
result[column] = { $in: values };
return result;
}
// 比较运算符
const op = this.parseComparisonOp();
// 尝试解析列引用(identifier DOT identifier 格式)
let value: unknown;
if (
(this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
this.peekTokenIs(TokenType.DOT)
) {
const colRef = this.parseColumnRef();
value = { $col: colRef };
} else {
value = this.parseValue();
}
const result: WhereCondition = {};
result[column] = { [op]: value };
return result;
}
private peekTokenIs(type: TokenType): boolean {
return this.peekToken.type === type;
}
private parseComparisonOp(): string {
switch (this.curToken.type) {
case TokenType.EQ: this.nextToken(); return '$eq';
case TokenType.NEQ: this.nextToken(); return '$ne';
case TokenType.GT: this.nextToken(); return '$gt';
case TokenType.GTE: this.nextToken(); return '$gte';
case TokenType.LT: this.nextToken(); return '$lt';
case TokenType.LTE: this.nextToken(); return '$lte';
default:
throw this.error(`Expected comparison operator, got "${this.curToken.value}"`);
}
}
// ===================================================================
// 辅助解析
// ===================================================================
private parseColumnList(): string[] {
const cols: string[] = [];
cols.push(this.parseColumnRef());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
cols.push(this.parseColumnRef());
}
return cols;
}
/** 解析列引用:支持 'col'、'table.col' 和 'COUNT(*)'/'SUM(col)' 等 */
private parseColumnRef(): string {
// 聚合函数?
if (
this.curTokenIs(TokenType.COUNT) ||
this.curTokenIs(TokenType.SUM) ||
this.curTokenIs(TokenType.AVG) ||
this.curTokenIs(TokenType.MIN) ||
this.curTokenIs(TokenType.MAX)
) {
return this.parseAggregateCall();
}
const first = this.expectIdentifier('column name');
if (this.curTokenIs(TokenType.DOT)) {
this.nextToken();
const second = this.expectIdentifier('column name');
return `${first}.${second}`;
}
return first;
}
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col) */
private parseAggregateCall(): string {
const func = this.curToken.value.toUpperCase();
this.nextToken();
this.expect(TokenType.LPAREN);
let arg: string;
if (this.curTokenIs(TokenType.STAR)) {
arg = '*';
this.nextToken();
} else {
arg = this.parseColumnRef();
}
this.expect(TokenType.RPAREN);
// 可选别名: AS alias
let alias = '';
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && this._isAggregateAlias()) {
alias = this.curToken.value;
this.nextToken();
}
if (alias) {
return `${func}(${arg}) AS ${alias}`;
}
return `${func}(${arg})`;
}
private _isAggregateAlias(): boolean {
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
}
private parseIdentifierList(): string[] {
const ids: string[] = [];
ids.push(this.expectIdentifier('identifier'));
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
ids.push(this.expectIdentifier('identifier'));
}
return ids;
}
private parseValueList(): unknown[] {
const vals: unknown[] = [];
vals.push(this.parseValue());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
vals.push(this.parseValue());
}
return vals;
}
private parseOrderByList(): OrderBy[] {
const list: OrderBy[] = [];
list.push(this.parseOrderBy());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
list.push(this.parseOrderBy());
}
return list;
}
private parseOrderBy(): OrderBy {
const column = this.expectIdentifier('column name');
let direction: SortDirection = 'asc';
if (this.curTokenIs(TokenType.ASC)) {
this.nextToken();
} else if (this.curTokenIs(TokenType.DESC)) {
direction = 'desc';
this.nextToken();
}
return { column, direction };
}
/** 解析字面量值 */
private parseValue(): unknown {
switch (this.curToken.type) {
case TokenType.STRING: {
const val = this.curToken.value;
this.nextToken();
return val;
}
case TokenType.NUMBER: {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
case TokenType.TRUE: this.nextToken(); return true;
case TokenType.FALSE: this.nextToken(); return false;
case TokenType.NULL: this.nextToken(); return null;
default:
throw this.error(`Expected value, got "${this.curToken.value}"`);
}
}
// ===================================================================
// Token 操作
// ===================================================================
private nextToken(): void {
this.curToken = this.peekToken;
this.peekToken = this.lexer.nextToken();
}
private curTokenIs(type: TokenType): boolean {
return this.curToken.type === type;
}
private expect(type: TokenType): void {
if (this.curTokenIs(type)) {
this.nextToken();
return;
}
throw this.error(`Expected ${type}, got "${this.curToken.value}"`);
}
private expectIdentifier(context: string): string {
if (this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) {
const val = this.curToken.value;
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
/** 关键字可以作为标识符(如列名等于关键字) */
private _isKeywordAsIdent(): boolean {
return (
this.curToken.type !== TokenType.EOF &&
this.curToken.type !== TokenType.ILLEGAL &&
this.curToken.type !== TokenType.STRING &&
this.curToken.type !== TokenType.NUMBER &&
this.curToken.type !== TokenType.COMMA &&
this.curToken.type !== TokenType.LPAREN &&
this.curToken.type !== TokenType.RPAREN &&
this.curToken.type !== TokenType.SEMICOLON &&
this.curToken.type !== TokenType.EQ &&
this.curToken.type !== TokenType.NEQ &&
this.curToken.type !== TokenType.GT &&
this.curToken.type !== TokenType.GTE &&
this.curToken.type !== TokenType.LT &&
this.curToken.type !== TokenType.LTE &&
this.curToken.type !== TokenType.DOT &&
this.curToken.type !== TokenType.STAR
);
}
private expectNumber(context: string): number {
if (this.curToken.type === TokenType.NUMBER) {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
private error(msg: string): DatabaseError {
return new DatabaseError(
`Parse error at position ${this.curToken.position}: ${msg}`,
'PARSE_ERROR',
);
}
}
// ---------------------------------------------------------------------------
// 便捷方法
// ---------------------------------------------------------------------------
/** 解析 SQL 字符串为 AST Statement */
export function parse(sql: string): Statement {
const parser = new Parser(sql);
const stmt = parser.parseStatement();
return stmt;
}
+152
View File
@@ -0,0 +1,152 @@
/**
* metona-sqlark SQL Token Types — 词法单元定义
* @module sql/tokens
*/
// ---------------------------------------------------------------------------
// Token 类型枚举
// ---------------------------------------------------------------------------
export enum TokenType {
// 关键字
SELECT = 'SELECT',
FROM = 'FROM',
WHERE = 'WHERE',
INSERT = 'INSERT',
INTO = 'INTO',
VALUES = 'VALUES',
UPDATE = 'UPDATE',
SET = 'SET',
DELETE = 'DELETE',
CREATE = 'CREATE',
TABLE = 'TABLE',
DROP = 'DROP',
ORDER = 'ORDER',
BY = 'BY',
ASC = 'ASC',
DESC = 'DESC',
LIMIT = 'LIMIT',
OFFSET = 'OFFSET',
AND = 'AND',
OR = 'OR',
NOT = 'NOT',
LIKE = 'LIKE',
IN = 'IN',
PRIMARY = 'PRIMARY',
KEY = 'KEY',
UNIQUE = 'UNIQUE',
DEFAULT = 'DEFAULT',
NULL = 'NULL',
TRUE = 'TRUE',
FALSE = 'FALSE',
// JOIN 相关
INNER = 'INNER',
LEFT = 'LEFT',
RIGHT = 'RIGHT',
CROSS = 'CROSS',
JOIN = 'JOIN',
ON = 'ON',
AS = 'AS',
OUTER = 'OUTER',
// 聚合
GROUP = 'GROUP',
HAVING = 'HAVING',
COUNT = 'COUNT',
SUM = 'SUM',
AVG = 'AVG',
MIN = 'MIN',
MAX = 'MAX',
DISTINCT = 'DISTINCT',
// 标识符 & 字面量
IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING',
NUMBER = 'NUMBER',
// 运算符 & 分隔符
COMMA = 'COMMA',
LPAREN = 'LPAREN',
RPAREN = 'RPAREN',
SEMICOLON = 'SEMICOLON',
EQ = 'EQ',
NEQ = 'NEQ',
GT = 'GT',
GTE = 'GTE',
LT = 'LT',
LTE = 'LTE',
STAR = 'STAR',
DOT = 'DOT',
// 特殊
EOF = 'EOF',
ILLEGAL = 'ILLEGAL',
}
// ---------------------------------------------------------------------------
// Token
// ---------------------------------------------------------------------------
export interface Token {
type: TokenType;
value: string;
position: number; // 在源码中的位置
}
// ---------------------------------------------------------------------------
// 关键字映射
// ---------------------------------------------------------------------------
export const KEYWORDS: Record<string, TokenType> = {
'SELECT': TokenType.SELECT,
'FROM': TokenType.FROM,
'WHERE': TokenType.WHERE,
'INSERT': TokenType.INSERT,
'INTO': TokenType.INTO,
'VALUES': TokenType.VALUES,
'UPDATE': TokenType.UPDATE,
'SET': TokenType.SET,
'DELETE': TokenType.DELETE,
'CREATE': TokenType.CREATE,
'TABLE': TokenType.TABLE,
'DROP': TokenType.DROP,
'ORDER': TokenType.ORDER,
'BY': TokenType.BY,
'ASC': TokenType.ASC,
'DESC': TokenType.DESC,
'LIMIT': TokenType.LIMIT,
'OFFSET': TokenType.OFFSET,
'AND': TokenType.AND,
'OR': TokenType.OR,
'NOT': TokenType.NOT,
'LIKE': TokenType.LIKE,
'IN': TokenType.IN,
'PRIMARY': TokenType.PRIMARY,
'KEY': TokenType.KEY,
'UNIQUE': TokenType.UNIQUE,
'DEFAULT': TokenType.DEFAULT,
'NULL': TokenType.NULL,
'TRUE': TokenType.TRUE,
'FALSE': TokenType.FALSE,
// JOIN
'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT,
'RIGHT': TokenType.RIGHT,
'CROSS': TokenType.CROSS,
'JOIN': TokenType.JOIN,
'ON': TokenType.ON,
'AS': TokenType.AS,
'OUTER': TokenType.OUTER,
// 聚合
'GROUP': TokenType.GROUP,
'HAVING': TokenType.HAVING,
'COUNT': TokenType.COUNT,
'SUM': TokenType.SUM,
'AVG': TokenType.AVG,
'MIN': TokenType.MIN,
'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT,
};
+7
View File
@@ -0,0 +1,7 @@
/**
* metona-sqlark Table — 表管理
* @module table
*/
export { createSchema, validateColumns, validateRow, getPrimaryKey } from './schema';
export { Table } from './table';
+172
View File
@@ -0,0 +1,172 @@
/**
* metona-sqlark Schema — 表结构定义与校验
* @module table/schema
*/
import type { TableSchema, ColumnDef, FieldType } from '../constants';
import { FIELD_TYPES, DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Schema 工具
// ---------------------------------------------------------------------------
/** 从列定义创建 TableSchema */
export function createSchema(name: string, columns: Record<string, ColumnDef>): TableSchema {
validateColumns(columns);
return { name, columns };
}
/** 校验列定义 */
export function validateColumns(columns: Record<string, ColumnDef>): void {
const colNames = Object.keys(columns);
if (colNames.length === 0) {
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
}
let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) {
// 类型校验
if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(
`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`,
'SCHEMA_ERROR',
);
}
// 主键计数
if (colDef.primaryKey) {
primaryKeyCount++;
}
}
// 至少需要一个主键
if (primaryKeyCount === 0) {
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
}
}
/** 获取主键列名 */
export function getPrimaryKey(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) {
if (col.primaryKey) return name;
}
return Object.keys(schema.columns)[0];
}
/** 校验行数据 */
export function validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
const validated: Record<string, unknown> = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
// 默认值
if (value === undefined && colDef.default !== undefined) {
value = colDef.default;
}
// 必填检查
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(
`Column "${colName}" is required in table "${schema.name}"`,
'VALIDATION_ERROR',
);
}
// 类型检查
if (value !== undefined && value !== null) {
checkFieldType(schema.name, colName, colDef.type, value, colDef);
}
if (value !== undefined) {
validated[colName] = value;
}
}
return validated;
}
/** 检查字段类型 */
export function checkFieldType(
tableName: string,
colName: string,
type: FieldType,
value: unknown,
_colDef?: ColumnDef,
): void {
const jsType = typeof value;
switch (type) {
case 'string':
if (jsType !== 'string') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'number':
if (jsType !== 'number') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'boolean':
if (jsType !== 'boolean') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'date':
if (jsType !== 'string' || isNaN(Date.parse(value as string))) {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`,
'TYPE_ERROR',
);
}
break;
case 'json':
if (jsType !== 'object') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
}
}
/** 将 AST 列定义转换为 ColumnDef */
export function astColumnToColumnDef(astCol: {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
}): ColumnDef {
return {
type: astCol.type as FieldType,
primaryKey: astCol.primaryKey,
unique: astCol.unique,
required: astCol.required,
default: astCol.default,
index: astCol.index,
maxLength: astCol.maxLength,
min: astCol.min,
max: astCol.max,
};
}
+83
View File
@@ -0,0 +1,83 @@
/**
* metona-sqlark Table — 表操作 API
* @module table/table
*/
import type { IStorageEngine } from '../engine/interface';
import type { TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../query/builder';
import { QueryExecutor } from '../query/executor';
// ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
export class Table<T = Record<string, unknown>> {
readonly name: string;
private engine: IStorageEngine;
private schema: TableSchema | null = null;
private executor: QueryExecutor | undefined;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor) {
this.engine = engine;
this.name = tableName;
this.executor = executor;
}
// ---- Schema ----
async getSchema(): Promise<TableSchema> {
if (!this.schema) {
const s = await this.engine.getTableSchema(this.name);
if (!s) throw new DatabaseError(`Table "${this.name}" does not exist`, 'TABLE_NOT_FOUND');
this.schema = s;
}
return this.schema;
}
// ---- 插入 ----
async insert(row: T & Record<string, unknown>): Promise<string> {
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
return pks[0];
}
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
return this.engine.insert(this.name, rows as Record<string, unknown>[]);
}
// ---- 查询 ----
select(columns: string[] = ['*']): SelectQueryBuilder {
return new SelectQueryBuilder(this.engine, this.name, columns, this.executor);
}
// ---- 更新 ----
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
return new UpdateQueryBuilder(this.engine, this.name, updates);
}
// ---- 删除 ----
delete(): DeleteQueryBuilder {
return new DeleteQueryBuilder(this.engine, this.name);
}
// ---- 聚合 ----
async count(where?: Record<string, unknown>): Promise<number> {
return this.engine.count(this.name, where ? { table: this.name, where } : undefined);
}
// ---- 管理 ----
async clear(): Promise<void> {
return this.engine.clear(this.name);
}
async drop(): Promise<void> {
return this.engine.dropTable(this.name);
}
}
+67
View File
@@ -0,0 +1,67 @@
/**
* metona-sqlark Transaction — 事务管理
* @module transaction
*
* 封装多操作事务,保证原子性。
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
*/
import type { IStorageEngine } from '../engine/interface';
import { Table } from '../table/table';
import { DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Transaction
// ---------------------------------------------------------------------------
export class Transaction {
private engine: IStorageEngine;
private tables: Map<string, Table> = new Map();
private completed = false;
constructor(engine: IStorageEngine) {
this.engine = engine;
}
/** 获取表操作对象 */
table(tableName: string): Table {
let t = this.tables.get(tableName);
if (!t) {
t = new Table(this.engine, tableName);
this.tables.set(tableName, t);
}
return t;
}
/** 标记事务完成(由 TransactionManager 调用) */
_markCompleted(): void {
this.completed = true;
}
/** 是否已完成 */
isCompleted(): boolean {
return this.completed;
}
}
// ---------------------------------------------------------------------------
// TransactionManager
// ---------------------------------------------------------------------------
export class TransactionManager {
constructor(private engine: IStorageEngine) {}
/** 执行事务 */
async execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T> {
const trx = new Transaction(this.engine);
try {
const result = await fn(trx);
trx._markCompleted();
return result;
} catch (error) {
if (error instanceof DatabaseError) throw error;
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
}
}
}
+80
View File
@@ -0,0 +1,80 @@
/**
* YOUR-PROJECT Utils — 工具函数
* @module utils
*/
/** 生成唯一 ID */
export const generateId = (): string => {
return 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
};
/** HTML 转义 */
export const escapeHTML = (s: unknown): string => {
const str = String(s == null ? '' : s);
if (typeof document === 'undefined') {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
/** 防抖 */
export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number,
immediate = false,
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null;
return function (this: any, ...args: Parameters<T>) {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/** 节流 */
export function throttle<T extends (...args: any[]) => void>(
func: T,
limit: number,
): (...args: Parameters<T>) => void {
let inThrottle = false;
return function (this: any, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
/** 深度合并 */
export const deepMerge = <T extends Record<string, unknown>>(target: T, source: Partial<T>): T => {
const output: Record<string, unknown> = { ...target };
for (const key of Object.keys(source as Record<string, unknown>)) {
const sv = (source as Record<string, unknown>)[key];
const tv = (target as Record<string, unknown>)[key];
if (sv instanceof Object && key in target && tv instanceof Object) {
output[key] = deepMerge(tv as Record<string, unknown>, sv as Record<string, unknown>);
} else {
output[key] = source[key];
}
}
return output as T;
};
/** 浏览器环境检测 */
export const isBrowser = (): boolean => {
return typeof window !== 'undefined' && typeof document !== 'undefined';
};
+134
View File
@@ -0,0 +1,134 @@
/**
* Core 全覆盖测试 — 导入导出、迁移、发布订阅、钩子
*/
import { MetonaSqlark } from '../src/core';
describe('Core 全覆盖', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-coverage', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('users').insertMany([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
});
afterEach(async () => { await db.close(); });
// ---- 导入导出 ----
describe('导入导出', () => {
it('exportTable 导出单表', async () => {
const data = await db.exportTable('users');
expect(data).toHaveLength(2);
expect(data[0].name).toBe('Alice');
});
it('exportAll 导出全库', async () => {
const all = await db.exportAll();
expect(all.users).toHaveLength(2);
});
it('importTable 导入数据', async () => {
await db.defineTable('temp', { id: { type: 'string', primaryKey: true }, val: { type: 'number' } });
await db.importTable('temp', [{ id: 'a', val: 1 }, { id: 'b', val: 2 }]);
expect(await db.table('temp').count()).toBe(2);
});
});
// ---- 发布订阅 ----
describe('发布订阅', () => {
it('subscribe 注册监听', () => {
const fn = jest.fn();
const unsub = db.subscribe('users', fn);
expect(typeof unsub).toBe('function');
});
it('unsubscribe 取消监听', () => {
const fn = jest.fn();
const unsub = db.subscribe('users', fn);
unsub();
db.emit('users', { type: 'insert', row: { id: '3' } });
expect(fn).not.toHaveBeenCalled();
});
it('emit 触发监听', () => {
const fn = jest.fn();
db.subscribe('users', fn);
db.emit('users', { type: 'insert', row: { id: '3', name: 'Charlie' } });
expect(fn).toHaveBeenCalledWith({ type: 'insert', row: { id: '3', name: 'Charlie' } });
});
});
// ---- 迁移 ----
describe('迁移系统', () => {
it('addMigration + migrateTo', async () => {
let migrated = false;
db.addMigration(2, async (d) => {
migrated = true;
await d.defineTable('new_table', { id: { type: 'string', primaryKey: true } });
});
await db.migrateTo(2);
expect(migrated).toBe(true);
const tables = await db.getTableNames();
expect(tables).toContain('new_table');
});
it('不执行已过期的迁移', async () => {
let count = 0;
db.addMigration(1, async () => { count++; }); // version <= current
db.addMigration(2, async () => { count++; });
await db.migrateTo(2);
// version 1 <= current version 1, should be skipped
expect(count).toBe(1);
});
});
// ---- 钩子 ----
describe('钩子系统', () => {
it('on 注册钩子 (通过 pluginManager)', () => {
const fn = jest.fn();
db.getPluginManager().on('beforeInsert', fn);
db.getPluginManager().trigger('beforeInsert', { id: '3' });
expect(fn).toHaveBeenCalled();
});
it('getPluginManager 返回插件管理器', () => {
const pm = db.getPluginManager();
expect(pm).toBeDefined();
expect(typeof pm.on).toBe('function');
});
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('close 关闭数据库', async () => {
await db.close();
expect(db.isReady()).toBe(false);
});
it('getEngine 返回引擎', () => {
const engine = db.getEngine();
expect(engine).toBeDefined();
expect(engine.name).toBe('memory');
});
it('未初始化时操作抛出错误', () => {
const db2 = new MetonaSqlark({ name: 'x', mode: 'memory' });
expect(() => db2.table('x')).toThrow('not initialized');
});
});
// ---- 别名 ----
describe('MeSqlark 别名', () => {
it('MeSqlark === MetonaSqlark', () => {
const { MeSqlark } = require('../src/index');
expect(MeSqlark).toBe(MetonaSqlark);
});
});
});
+293
View File
@@ -0,0 +1,293 @@
/**
* MetonaSqlark 核心集成测试
*/
import { MetonaSqlark } from '../src/core';
import 'fake-indexeddb/auto';
let diskCounter = 0;
// ===================================================================
// Memory 模式
// ===================================================================
describe('MetonaSqlark Memory 模式', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-db-mem', mode: 'memory' });
await db.init();
});
afterEach(async () => {
await db.close();
});
it('创建数据库并初始化', () => {
expect(db.isReady()).toBe(true);
expect(db.name).toBe('test-db-mem');
expect(db.mode).toBe('memory');
});
it('定义表', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
});
const tables = await db.getTableNames();
expect(tables).toContain('users');
});
describe('Query Builder API', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
});
});
it('insert + select', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
await table.insert({ id: '2', name: 'Bob', age: 25 });
const results = await table.select().where({ age: { $gt: 20 } }).execute();
expect(results).toHaveLength(2);
});
it('insertMany', async () => {
const table = db.table('users');
await table.insertMany([
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
const count = await table.count();
expect(count).toBe(2);
});
it('update', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
const count = await table.update({ age: 31 }).where({ id: '1' }).execute();
expect(count).toBe(1);
const results = await table.select().where({ id: '1' }).execute();
expect(results[0].age).toBe(31);
});
it('delete', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
const count = await table.delete().where({ id: '1' }).execute();
expect(count).toBe(1);
expect(await table.count()).toBe(0);
});
it('orderBy + limit + offset', async () => {
const table = db.table('users');
await table.insertMany([
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Charlie', age: 35 },
]);
const results = await table.select().orderBy('age', 'desc').limit(2).offset(1).execute();
expect(results).toHaveLength(2);
});
it('dropTable', async () => {
await db.dropTable('users');
const tables = await db.getTableNames();
expect(tables).not.toContain('users');
});
});
describe('SQL API', () => {
beforeEach(async () => {
await db.query('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)');
});
it('SQL INSERT + SELECT', async () => {
await db.query("INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('SQL SELECT WHERE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
const result = await db.query("SELECT * FROM users WHERE age > 28") as Record<string, unknown>[];
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
it('SQL UPDATE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("UPDATE users SET age = 31 WHERE id = '1'");
const result = await db.query("SELECT * FROM users WHERE id = '1'") as Record<string, unknown>[];
expect(result[0].age).toBe(31);
});
it('SQL DELETE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("DELETE FROM users WHERE id = '1'");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(0);
});
it('SQL DROP TABLE', async () => {
await db.query('DROP TABLE users');
const tables = await db.getTableNames();
expect(tables).not.toContain('users');
});
});
describe('事务', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
});
});
it('事务中执行多个操作', async () => {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
const table = db.table('users');
expect(await table.count()).toBe(2);
});
});
describe('表管理高级', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
});
it('getTableNames 返回所有表', async () => {
const names = await db.getTableNames();
expect(names).toContain('users');
});
it('dropTable 后表不再存在', async () => {
await db.dropTable('users');
const names = await db.getTableNames();
expect(names).not.toContain('users');
});
it('Table.getSchema 返回表结构', async () => {
const table = db.table('users');
const schema = await table.getSchema();
expect(schema.name).toBe('users');
expect(schema.columns.id.primaryKey).toBe(true);
});
it('Table.clear 清空数据', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice' });
await table.insert({ id: '2', name: 'Bob' });
await table.clear();
expect(await table.count()).toBe(0);
expect(await db.getTableNames()).toContain('users');
});
it('Table.drop 删除表', async () => {
const table = db.table('users');
await table.drop();
const names = await db.getTableNames();
expect(names).not.toContain('users');
});
});
});
// ===================================================================
// Disk 模式
// ===================================================================
describe('MetonaSqlark Disk 模式', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `test-disk-${++diskCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'indexeddb' });
await db.init();
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('创建 disk 模式数据库', () => {
expect(db.isReady()).toBe(true);
expect(db.mode).toBe('disk');
});
it('定义表并插入数据', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('users').insert({ id: '1', name: 'Alice' });
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(1);
});
});
// ===================================================================
// Hybrid 模式
// ===================================================================
describe('MetonaSqlark Hybrid 模式', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `test-hybrid-core-${++diskCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
await db.init();
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('创建 hybrid 模式数据库', () => {
expect(db.isReady()).toBe(true);
expect(db.mode).toBe('hybrid');
});
it('基本 CRUD', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice' });
expect(await table.count()).toBe(1);
await table.update({ name: 'Alice2' }).where({ id: '1' }).execute();
const rows = await table.select().where({ id: '1' }).execute();
expect(rows[0].name).toBe('Alice2');
await table.delete().where({ id: '1' }).execute();
expect(await table.count()).toBe(0);
});
it('SQL 查询', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(1);
});
});
+210
View File
@@ -0,0 +1,210 @@
/**
* Executor + Builder + Memory 边缘覆盖测试
*/
import { MetonaSqlark } from '../src/core';
import { MemoryEngine } from '../src/engine/memory';
import { SelectQueryBuilder } from '../src/query/builder';
import { createSchema } from '../src/table/schema';
describe('边缘覆盖', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-edge', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
active: { type: 'boolean', default: true },
});
await db.table('users').insertMany([
{ id: '1', name: 'Alice', age: 30, active: true },
{ id: '2', name: 'Bob', age: 25, active: true },
{ id: '3', name: 'Charlie', age: 35, active: false },
{ id: '4', name: 'Diana', age: 30, active: true },
]);
});
afterEach(async () => { await db.close(); });
// ---- DISTINCT ----
describe('DISTINCT', () => {
it('SELECT DISTINCT 去重', async () => {
const result = await db.query('SELECT DISTINCT age FROM users ORDER BY age') as Record<string, unknown>[];
expect(result).toHaveLength(3); // 30,25,35
});
it('DISTINCT with WHERE', async () => {
const result = await db.query("SELECT DISTINCT age FROM users WHERE active = TRUE") as Record<string, unknown>[];
expect(result).toHaveLength(2); // 30,25
});
});
// ---- $and / $or / $not ----
describe('复杂 Where 条件', () => {
it('$and 条件 (MemoryEngine 直接)', async () => {
// $and 在 MemoryEngine 的 matchWhere 中解析
const engine = new MemoryEngine();
await engine.open('test', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, age: { type: 'number' }, active: { type: 'boolean' },
}));
await engine.insert('users', [
{ id: '1', age: 30, active: true },
{ id: '2', age: 25, active: true },
{ id: '3', age: 30, active: false },
]);
const rows = await engine.find('users', {
table: 'users',
where: { $and: [{ age: 30 }, { active: true }] } as any,
});
expect(rows).toHaveLength(1);
await engine.close();
});
it('$or 条件 (MemoryEngine 直接)', async () => {
const engine = new MemoryEngine();
await engine.open('test2', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, name: { type: 'string' },
}));
await engine.insert('users', [
{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, { id: '3', name: 'Charlie' },
]);
const rows = await engine.find('users', {
table: 'users',
where: { $or: [{ name: 'Alice' }, { name: 'Bob' }] } as any,
});
expect(rows).toHaveLength(2);
await engine.close();
});
it('$not 条件', async () => {
const rows = await db.table('users').select().where({
name: { $not: { $eq: 'Alice' } } as any,
}).execute();
expect(rows).toHaveLength(3);
});
it('$in + $nin', async () => {
const r1 = await db.table('users').select().where({ age: { $in: [25, 35] } }).execute();
expect(r1).toHaveLength(2);
const r2 = await db.table('users').select().where({ age: { $nin: [30] } }).execute();
expect(r2).toHaveLength(2);
});
it('$lt + $lte', async () => {
const r = await db.table('users').select().where({ age: { $lt: 30 } }).execute();
expect(r).toHaveLength(1); // Bob(25)
const r2 = await db.table('users').select().where({ age: { $lte: 30 } }).execute();
expect(r2).toHaveLength(3); // Alice+Diana(30)+Bob(25)
});
});
// ---- QueryBuilder 全覆盖 ----
describe('QueryBuilder 全覆盖', () => {
let engine: MemoryEngine;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
}));
await engine.insert('users', [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
});
afterEach(async () => { await engine.close(); });
it('SelectQueryBuilder.as 别名', () => {
const qb = new SelectQueryBuilder(engine, 'users').as('u');
expect(qb.toAST().alias).toBe('u');
});
it('rightJoin', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.rightJoin('orders', { 'users.id': { $col: 'orders.user_id' } }, 'o');
expect(qb.toAST().joins![0].type).toBe('RIGHT');
});
it('join 默认为 INNER', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.join('orders', { 'users.id': { $col: 'orders.user_id' } });
expect(qb.toAST().joins![0].type).toBe('INNER');
});
});
// ---- RIGHT JOIN SQL ----
describe('RIGHT JOIN', () => {
beforeEach(async () => {
await db.defineTable('departments', {
id: { type: 'number', primaryKey: true },
name: { type: 'string' },
});
await db.table('departments').insertMany([
{ id: 1, name: 'Eng' }, { id: 2, name: 'Sales' },
]);
await db.defineTable('emp', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
dept_id: { type: 'number' },
});
await db.table('emp').insert({ id: '1', name: 'Alice', dept_id: 1 });
});
it('RIGHT JOIN 保留右表无匹配行', async () => {
const result = await db.query(
'SELECT emp.name, departments.name FROM emp RIGHT JOIN departments ON emp.dept_id = departments.id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(2);
});
});
// ---- MemoryEngine 索引 ----
describe('MemoryEngine 索引查找', () => {
it('索引列 $eq 查询', async () => {
// age 列标记为 index
await db.defineTable('indexed_users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
score: { type: 'number', index: true },
});
await db.table('indexed_users').insertMany([
{ id: '1', name: 'A', score: 100 },
{ id: '2', name: 'B', score: 200 },
{ id: '3', name: 'C', score: 100 },
]);
// $eq 查询应使用索引
const rows = await db.table('indexed_users').select().where({ score: 100 }).execute();
expect(rows).toHaveLength(2);
});
it('唯一索引列查询', async () => {
await db.defineTable('unique_users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.table('unique_users').insertMany([
{ id: '1', email: 'a@t.com' },
{ id: '2', email: 'b@t.com' },
]);
const rows = await db.table('unique_users').select().where({ email: 'a@t.com' }).execute();
expect(rows).toHaveLength(1);
});
});
// ---- GROUP BY 边缘 ----
describe('GROUP BY 边缘', () => {
it('单个 GROUP BY 列', async () => {
const result = await db.query('SELECT active, COUNT(*) FROM users GROUP BY active') as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('AVG 空值处理 (GROUP BY)', async () => {
await db.defineTable('scores', { id: { type: 'string', primaryKey: true }, dept: { type: 'string' }, val: { type: 'number' } });
await db.table('scores').insertMany([{ id: '1', dept: 'A', val: 100 }, { id: '2', dept: 'A', val: 200 }]);
const result = await db.query('SELECT dept, AVG(val) FROM scores GROUP BY dept') as Record<string, unknown>[];
expect(result[0]['AVG(val)']).toBe(150);
});
});
});
+323
View File
@@ -0,0 +1,323 @@
/**
* IndexedDBEngine 完整测试(使用 fake-indexeddb
* 每次测试使用独立的数据库名避免版本冲突
*/
import 'fake-indexeddb/auto';
import { IndexedDBEngine } from '../../src/engine/indexeddb';
import { createSchema } from '../../src/table/schema';
let dbCounter = 0;
describe('IndexedDBEngine', () => {
let engine: IndexedDBEngine;
let dbName: string;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
});
beforeEach(async () => {
dbName = `test-idb-${++dbCounter}`;
engine = new IndexedDBEngine();
await engine.open(dbName, 1);
});
afterEach(async () => {
if (engine.isOpen()) {
await engine.close();
}
// 清理:删除 IndexedDB 数据库
try {
indexedDB.deleteDatabase(dbName);
} catch {}
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('打开和关闭', async () => {
expect(engine.isOpen()).toBe(true);
await engine.close();
expect(engine.isOpen()).toBe(false);
});
it('未打开时操作抛出错误', async () => {
await engine.close();
await expect(engine.count('users')).rejects.toThrow('not opened');
});
it('可以重新打开', async () => {
await engine.close();
await engine.open(dbName, 2);
expect(engine.isOpen()).toBe(true);
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建和删除表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
it('表名列表', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.name).toBe('users');
});
it('获取不存在的表结构返回 null', async () => {
const schema = await engine.getTableSchema('nonexistent');
expect(schema).toBeNull();
});
});
// ---- 插入 ----
describe('插入', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入单行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
]);
expect(pks).toEqual(['1']);
expect(await engine.count('users')).toBe(1);
});
it('插入多行', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
expect(await engine.count('users')).toBe(2);
});
it('必填字段缺失抛出错误', async () => {
await expect(
engine.insert('users', [{ id: '1', email: 'a@t.com' }]),
).rejects.toThrow('required');
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@test.com' },
]);
});
it('查询所有', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE 条件', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 28 } },
});
expect(rows).toHaveLength(2);
});
it('ORDER BY + LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
limit: 2,
});
expect(rows.map(r => r.age)).toEqual([35, 30]);
});
it('列选择', async () => {
const rows = await engine.find('users', {
table: 'users',
columns: ['id'],
where: { id: '1' },
});
expect(Object.keys(rows[0])).toEqual(['id']);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('更新匹配行', async () => {
const count = await engine.update(
'users',
{ table: 'users', where: { id: '1' } },
{ age: 31 },
);
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('更新所有行', async () => {
const count = await engine.update(
'users',
{ table: 'users' },
{ age: 100 },
);
expect(count).toBe(2);
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('条件删除', async () => {
const count = await engine.delete('users', {
table: 'users',
where: { id: '1' },
});
expect(count).toBe(1);
expect(await engine.count('users')).toBe(1);
});
it('清空表', async () => {
await engine.clear('users');
expect(await engine.count('users')).toBe(0);
// 表结构仍存在
expect(await engine.hasTable('users')).toBe(true);
});
it('删除不存在的表抛出错误', async () => {
await expect(
engine.delete('nonexistent', { table: 'nonexistent' }),
).rejects.toThrow();
});
});
// ---- Count ----
describe('Count', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('count 全部', async () => {
expect(await engine.count('users')).toBe(2);
});
it('count with where', async () => {
expect(await engine.count('users', {
table: 'users',
where: { age: { $gt: 28 } },
})).toBe(1);
});
});
// ---- 复杂 Where 条件 ----
describe('复杂 Where', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'c@t.com' },
]);
});
it('$like 模糊匹配', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'A%' } },
});
expect(rows).toHaveLength(1);
});
it('$in 列表', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { id: { $in: ['1', '3'] } },
});
expect(rows).toHaveLength(2);
});
it('$ne 不等于', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $ne: 25 } },
});
expect(rows).toHaveLength(2);
});
it('$gte $lte 范围', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gte: 25, $lte: 30 } },
});
expect(rows).toHaveLength(2);
});
});
// ---- 并发/多次操作 ----
describe('多次操作', () => {
it('连续创建删除表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('插入后查询再更新再查询', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
]);
let rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(30);
await engine.update('users', { table: 'users' }, { age: 31 });
rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(31);
});
});
});
+272
View File
@@ -0,0 +1,272 @@
/**
* MemoryEngine 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { createSchema } from '../../src/table/schema';
describe('MemoryEngine', () => {
let engine: MemoryEngine;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
});
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-db', 1);
});
afterEach(async () => {
await engine.close();
});
// ---- 生命周期 ----
describe('open/close', () => {
it('打开后 isOpen 返回 true', () => {
expect(engine.isOpen()).toBe(true);
});
it('关闭后 isOpen 返回 false', async () => {
await engine.close();
expect(engine.isOpen()).toBe(false);
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('重复创建表抛出错误', async () => {
await engine.createTable(userSchema);
await expect(engine.createTable(userSchema)).rejects.toThrow('already exists');
});
it('获取所有表名', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
});
// ---- CRUD ----
describe('插入', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入单行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
]);
expect(pks).toEqual(['1']);
});
it('插入多行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
expect(pks).toEqual(['1', '2']);
});
it('重复主键抛出错误', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
await expect(
engine.insert('users', [{ id: '1', name: 'Duplicate', email: 'd@t.com' }]),
).rejects.toThrow('Duplicate');
});
it('唯一约束冲突', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'same@test.com' }]);
await expect(
engine.insert('users', [{ id: '2', name: 'Bob', email: 'same@test.com' }]),
).rejects.toThrow('Unique constraint');
});
it('必填字段缺失', async () => {
await expect(
engine.insert('users', [{ id: '1' }]),
).rejects.toThrow('required');
});
it('默认值填充', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(0);
});
it('类型错误', async () => {
await expect(
engine.insert('users', [{ id: '1', name: 'Alice', age: 'not-a-number', email: 'a@t.com' }]),
).rejects.toThrow('expects number');
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@test.com' },
]);
});
it('查询所有行', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE $gt', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 28 } },
});
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
});
it('WHERE $eq', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: 'Alice' },
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
});
it('WHERE $in', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $in: [25, 35] } },
});
expect(rows).toHaveLength(2);
});
it('WHERE $like', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'A%' } },
});
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
});
it('ORDER BY asc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'asc' }],
});
expect(rows.map((r) => r.age)).toEqual([25, 30, 35]);
});
it('ORDER BY desc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
});
expect(rows.map((r) => r.age)).toEqual([35, 30, 25]);
});
it('LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
limit: 2,
});
expect(rows).toHaveLength(2);
});
it('OFFSET', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'id', direction: 'asc' }],
offset: 1,
limit: 1,
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
});
it('列选择', async () => {
const rows = await engine.find('users', {
table: 'users',
columns: ['id', 'name'],
where: { id: '1' },
});
expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('更新单行', async () => {
const count = await engine.update('users',
{ table: 'users', where: { id: '1' } },
{ age: 31 },
);
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('更新所有行', async () => {
const count = await engine.update('users',
{ table: 'users' },
{ age: 100 },
);
expect(count).toBe(2);
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('条件删除', async () => {
const count = await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(1);
});
it('清空表', async () => {
await engine.clear('users');
const count = await engine.count('users');
expect(count).toBe(0);
});
});
});
+118
View File
@@ -0,0 +1,118 @@
/**
* GROUP BY + 聚合函数测试 (v0.1.2)
*/
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
describe('GROUP BY (v0.1.2)', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-groupby', mode: 'memory' });
await db.init();
await db.defineTable('employees', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
dept: { type: 'string' },
salary: { type: 'number' },
});
await db.table('employees').insertMany([
{ id: '1', name: 'Alice', dept: 'Eng', salary: 1000 },
{ id: '2', name: 'Bob', dept: 'Eng', salary: 1200 },
{ id: '3', name: 'Charlie', dept: 'Sales', salary: 900 },
{ id: '4', name: 'Diana', dept: 'Sales', salary: 1100 },
{ id: '5', name: 'Eve', dept: 'HR', salary: 800 },
]);
});
afterEach(async () => {
await db.close();
});
// ===================================================================
// SQL GROUP BY
// ===================================================================
describe('SQL GROUP BY', () => {
it('GROUP BY + COUNT', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['COUNT(*)']).toBe(2);
});
it('GROUP BY + SUM', async () => {
const result = await db.query(
'SELECT dept, SUM(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['SUM(salary)']).toBe(2200);
});
it('GROUP BY + AVG', async () => {
const result = await db.query(
'SELECT dept, AVG(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['AVG(salary)']).toBe(1100);
});
it('GROUP BY + MIN/MAX', async () => {
const result = await db.query(
'SELECT dept, MIN(salary), MAX(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['MIN(salary)']).toBe(1000);
expect(eng!['MAX(salary)']).toBe(1200);
});
it('GROUP BY + HAVING', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1',
) as Record<string, unknown>[];
expect(result).toHaveLength(2); // Eng(2) + Sales(2)
});
it('GROUP BY + WHERE + HAVING', async () => {
const result = await db.query(
"SELECT dept, SUM(salary) FROM employees WHERE dept != 'HR' GROUP BY dept HAVING SUM(salary) > 2000",
) as Record<string, unknown>[];
expect(result).toHaveLength(1); // Only Eng
});
it('聚合函数别名', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) AS cnt, SUM(salary) AS total FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!.cnt).toBe(2);
expect(eng!.total).toBe(2200);
});
});
// ===================================================================
// Parser 测试
// ===================================================================
describe('Parser', () => {
it('解析 COUNT(*)', () => {
const ast = parse('SELECT COUNT(*) FROM employees');
expect(ast.columns[0]).toBe('COUNT(*)');
});
it('解析 GROUP BY', () => {
const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept');
expect(ast.groupBy).toEqual(['dept']);
});
it('解析 HAVING', () => {
const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1');
expect(ast.having).toBeDefined();
});
});
});
+140
View File
@@ -0,0 +1,140 @@
/**
* HybridEngine 测试 — write-through 策略验证
* 注:re-open 持久化测试需要真实 IndexedDB 版本管理,fake-indexeddb 不支持。
* 这里验证 write-through 即时效正确性。
*/
import { HybridEngine } from '../../src/hybrid/index';
import { createSchema } from '../../src/table/schema';
import 'fake-indexeddb/auto';
let hybridCounter = 0;
describe('HybridEngine', () => {
let engine: HybridEngine;
let dbName: string;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
});
beforeEach(async () => {
dbName = `test-hybrid-${++hybridCounter}`;
engine = new HybridEngine('indexeddb');
await engine.open(dbName, 1);
});
afterEach(async () => {
if (engine.isOpen()) {
await engine.close();
}
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('引擎名称', () => {
expect(engine.name).toBe('hybrid');
});
it('磁盘引擎类型', () => {
expect(engine.getDiskEngineType()).toBe('indexeddb');
});
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
it('表名列表', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.name).toBe('users');
});
});
describe('Write-Through 即时性', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入后内存立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
const data = await engine.find('users', { table: 'users' });
expect(data).toHaveLength(2);
expect(await engine.count('users')).toBe(2);
});
it('更新后立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
]);
await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 31 });
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(31);
});
it('删除后立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(await engine.count('users')).toBe(1);
const rows = await engine.find('users', { table: 'users' });
expect(rows.map(r => r.id)).toEqual(['2']);
});
it('查询带排序和分页', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Charlie', age: 35 },
]);
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
limit: 2,
});
expect(rows.map(r => r.age)).toEqual([35, 30]);
});
it('clear 清空表', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
await engine.clear('users');
expect(await engine.count('users')).toBe(0);
});
});
describe('生命周期', () => {
it('isOpen 状态', async () => {
expect(engine.isOpen()).toBe(true);
await engine.close();
expect(engine.isOpen()).toBe(false);
});
});
describe('内存引擎访问', () => {
it('getMemoryEngine 返回内存引擎', () => {
const mem = engine.getMemoryEngine();
expect(mem.name).toBe('memory');
});
});
});
+209
View File
@@ -0,0 +1,209 @@
/**
* JOIN 功能测试 (v0.1.1)
*/
import { MetonaSqlark } from '../src/core';
describe('JOIN (v0.1.1)', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-join', mode: 'memory' });
await db.init();
// 创建测试表
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
dept_id: { type: 'number' },
});
await db.defineTable('departments', {
id: { type: 'number', primaryKey: true },
name: { type: 'string', required: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
// 插入测试数据
await db.table('users').insertMany([
{ id: '1', name: 'Alice', dept_id: 1 },
{ id: '2', name: 'Bob', dept_id: 1 },
{ id: '3', name: 'Charlie', dept_id: 2 },
]);
await db.table('departments').insertMany([
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Sales' },
{ id: 3, name: 'HR' }, // 无用户
]);
await db.table('orders').insertMany([
{ id: 'o1', user_id: '1', amount: 100 },
{ id: 'o2', user_id: '1', amount: 200 },
{ id: 'o3', user_id: '2', amount: 150 },
]);
});
afterEach(async () => {
await db.close();
});
// ===================================================================
// SQL JOIN
// ===================================================================
describe('SQL JOIN', () => {
it('INNER JOIN', async () => {
const result = await db.query(
'SELECT users.name, departments.name FROM users INNER JOIN departments ON users.dept_id = departments.id',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
});
it('LEFT JOIN', async () => {
const result = await db.query(
'SELECT departments.name FROM departments LEFT JOIN users ON departments.id = users.dept_id',
) as Record<string, unknown>[];
// HR 部门没有用户,LEFT JOIN 应保留
expect(result.length).toBeGreaterThanOrEqual(4);
});
it('CROSS JOIN', async () => {
const result = await db.query(
'SELECT * FROM users CROSS JOIN departments',
) as Record<string, unknown>[];
// 3 users × 3 departments = 9
expect(result).toHaveLength(9);
});
it('JOIN with alias', async () => {
const result = await db.query(
'SELECT u.name, d.name FROM users u INNER JOIN departments d ON u.dept_id = d.id',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
});
it('JOIN with AS alias', async () => {
const result = await db.query(
'SELECT u.name FROM users AS u INNER JOIN orders AS o ON u.id = o.user_id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(3);
});
it('JOIN with WHERE', async () => {
const result = await db.query(
'SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE o.amount > 120',
) as Record<string, unknown>[];
expect(result).toHaveLength(2); // o2(200) + o3(150)
});
it('Multiple JOIN', async () => {
const result = await db.query(
'SELECT u.name, d.name, o.amount FROM users u INNER JOIN departments d ON u.dept_id = d.id INNER JOIN orders o ON u.id = o.user_id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(3);
});
it('LEFT JOIN with no match returns null', async () => {
const result = await db.query(
'SELECT d.name, u.name FROM departments d LEFT JOIN users u ON d.id = u.dept_id WHERE d.id = 3',
) as Record<string, unknown>[];
expect(result).toHaveLength(1);
// HR 部门无用户,u.name 应为 null
// Note: column projection may vary
});
});
// ===================================================================
// QueryBuilder JOIN
// ===================================================================
describe('QueryBuilder JOIN', () => {
it('innerJoin via QueryBuilder', async () => {
const results = await db.table('users')
.select(['users.name', 'departments.name'])
.innerJoin('departments', { 'users.dept_id': { $col: 'departments.id' } })
.execute();
expect(results).toHaveLength(3);
});
it('leftJoin via QueryBuilder', async () => {
const results = await db.table('departments')
.select()
.leftJoin('users', { 'departments.id': { $col: 'users.dept_id' } })
.execute();
expect(results.length).toBeGreaterThanOrEqual(4);
});
it('crossJoin via QueryBuilder', async () => {
const results = await db.table('users')
.select()
.crossJoin('departments')
.execute();
expect(results).toHaveLength(9);
});
it('join with where + orderBy + limit', async () => {
const results = await db.table('users')
.select()
.innerJoin('orders', { 'users.id': { $col: 'orders.user_id' } })
.where({ 'orders.amount': { $gt: 100 } })
.orderBy('orders.amount', 'desc')
.limit(2)
.execute();
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results.length).toBeLessThanOrEqual(2);
// 按 amount desc 排序
if (results.length === 2) {
expect((results[0] as any)['orders.amount']).toBeGreaterThanOrEqual(
(results[1] as any)['orders.amount'],
);
}
});
});
// ===================================================================
// Parser JOIN 语法
// ===================================================================
describe('Parser JOIN 语法', () => {
it('INNER JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users INNER JOIN orders ON users.id = orders.user_id');
expect(ast.joins).toHaveLength(1);
expect(ast.joins[0].type).toBe('INNER');
expect(ast.joins[0].table).toBe('orders');
});
it('LEFT OUTER JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users LEFT OUTER JOIN orders ON users.id = orders.user_id');
expect(ast.joins[0].type).toBe('LEFT');
});
it('RIGHT JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users RIGHT JOIN orders ON users.id = orders.user_id');
expect(ast.joins[0].type).toBe('RIGHT');
});
it('CROSS JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users CROSS JOIN departments');
expect(ast.joins[0].type).toBe('CROSS');
});
it('表别名解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT u.name FROM users u');
expect(ast.alias).toBe('u');
});
it('AS 别名解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT u.name FROM users AS u');
expect(ast.alias).toBe('u');
});
});
});
+182
View File
@@ -0,0 +1,182 @@
/**
* PluginManager 完整测试
*/
import { PluginManager } from '../../src/plugin/index';
import type { MetonaPlugin } from '../../src/constants';
describe('PluginManager', () => {
let pm: PluginManager;
beforeEach(() => {
pm = new PluginManager();
});
// ---- 注册/卸载 ----
describe('注册和卸载', () => {
it('注册插件', () => {
const plugin: MetonaPlugin = {
name: 'test-plugin',
version: '1.0.0',
install: jest.fn(),
destroy: jest.fn(),
};
pm.register(plugin);
expect(pm.getPlugins()).toHaveLength(1);
expect(plugin.install).toHaveBeenCalled();
});
it('按优先级排序', () => {
const p1: MetonaPlugin = { name: 'low', version: '1.0', priority: 1, install: jest.fn(), destroy: jest.fn() };
const p2: MetonaPlugin = { name: 'high', version: '1.0', priority: 10, install: jest.fn(), destroy: jest.fn() };
const p3: MetonaPlugin = { name: 'mid', version: '1.0', priority: 5, install: jest.fn(), destroy: jest.fn() };
pm.register(p1);
pm.register(p2);
pm.register(p3);
const plugins = pm.getPlugins();
expect(plugins[0].name).toBe('high');
expect(plugins[1].name).toBe('mid');
expect(plugins[2].name).toBe('low');
});
it('卸载插件调用 destroy', () => {
const destroyFn = jest.fn();
const plugin: MetonaPlugin = {
name: 'test',
version: '1.0',
install: jest.fn(),
destroy: destroyFn,
};
pm.register(plugin);
pm.unregister('test');
expect(destroyFn).toHaveBeenCalled();
expect(pm.getPlugins()).toHaveLength(0);
});
it('卸载不存在的插件不报错', () => {
expect(() => pm.unregister('nonexistent')).not.toThrow();
});
it('getPlugins 返回副本', () => {
const plugin: MetonaPlugin = {
name: 'test', version: '1.0', install: jest.fn(), destroy: jest.fn(),
};
pm.register(plugin);
const plugins = pm.getPlugins();
plugins.push({ name: 'external', version: '1.0', install: jest.fn(), destroy: jest.fn() });
expect(pm.getPlugins()).toHaveLength(1);
});
});
// ---- 钩子 ----
describe('钩子系统', () => {
it('注册和触发钩子', async () => {
const callback = jest.fn();
pm.on('beforeInsert', callback);
await pm.trigger('beforeInsert', { id: '1' });
expect(callback).toHaveBeenCalledWith({ id: '1' });
});
it('多个回调', async () => {
const cb1 = jest.fn();
const cb2 = jest.fn();
pm.on('afterQuery', cb1);
pm.on('afterQuery', cb2);
await pm.trigger('afterQuery', 'SELECT *', []);
expect(cb1).toHaveBeenCalled();
expect(cb2).toHaveBeenCalled();
});
it('卸载钩子', async () => {
const cb = jest.fn();
pm.on('beforeUpdate', cb);
pm.off('beforeUpdate', cb);
await pm.trigger('beforeUpdate');
expect(cb).not.toHaveBeenCalled();
});
it('触发未注册的钩子不报错', async () => {
await expect(pm.trigger('beforeInsert')).resolves.toBeUndefined();
});
it('卸载不存在的回调不报错', () => {
expect(() => pm.off('beforeInsert', jest.fn())).not.toThrow();
});
it('异步回调', async () => {
const cb = jest.fn().mockResolvedValue(undefined);
pm.on('beforeQuery', cb);
await pm.trigger('beforeQuery');
expect(cb).toHaveBeenCalled();
});
});
// ---- 生命周期钩子覆盖 ----
describe('全部钩子类型', () => {
const hooks = [
'beforeCreateTable', 'afterCreateTable',
'beforeDropTable', 'afterDropTable',
'beforeInsert', 'afterInsert',
'beforeUpdate', 'afterUpdate',
'beforeDelete', 'afterDelete',
'beforeQuery', 'afterQuery',
'beforeTransaction', 'afterTransaction',
] as const;
hooks.forEach((hook) => {
it(`钩子 ${hook} 可注册和触发`, async () => {
const cb = jest.fn();
pm.on(hook, cb);
await pm.trigger(hook);
expect(cb).toHaveBeenCalledTimes(1);
});
});
});
// ---- destroy ----
describe('销毁', () => {
it('destroy 调用所有插件的 destroy', () => {
const d1 = jest.fn();
const d2 = jest.fn();
pm.register({ name: 'p1', version: '1.0', install: jest.fn(), destroy: d1 });
pm.register({ name: 'p2', version: '1.0', install: jest.fn(), destroy: d2 });
pm.destroy();
expect(d1).toHaveBeenCalled();
expect(d2).toHaveBeenCalled();
expect(pm.getPlugins()).toHaveLength(0);
});
it('destroy 清空钩子', async () => {
const cb = jest.fn();
pm.on('beforeInsert', cb);
pm.destroy();
await pm.trigger('beforeInsert');
expect(cb).not.toHaveBeenCalled();
});
it('插件 destroy 出错不影响销毁流程', () => {
const badPlugin: MetonaPlugin = {
name: 'bad',
version: '1.0',
install: jest.fn(),
destroy: () => { throw new Error('destroy failed'); },
};
const goodPlugin: MetonaPlugin = {
name: 'good',
version: '1.0',
install: jest.fn(),
destroy: jest.fn(),
};
pm.register(badPlugin);
pm.register(goodPlugin);
expect(() => pm.destroy()).not.toThrow();
expect(goodPlugin.destroy).toHaveBeenCalled();
});
});
});
+202
View File
@@ -0,0 +1,202 @@
/**
* QueryBuilder 和 Compiler/Executor 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { createSchema } from '../../src/table/schema';
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../../src/query/builder';
import { compileStatement } from '../../src/query/compiler';
import { QueryExecutor } from '../../src/query/executor';
import { parse } from '../../src/sql/parser';
describe('QueryBuilder', () => {
let engine: MemoryEngine;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-qb', 1);
});
afterEach(async () => {
await engine.close();
});
// ---- SelectQueryBuilder ----
describe('SelectQueryBuilder', () => {
it('基础查询', () => {
const qb = new SelectQueryBuilder(engine, 'users');
expect(qb.toAST()).toMatchObject({
type: 'SELECT',
columns: ['*'],
from: 'users',
where: {},
});
});
it('链式调用', () => {
const qb = new SelectQueryBuilder(engine, 'users', ['id', 'name']);
qb.where({ age: { $gt: 18 } }).orderBy('name', 'asc').limit(10).offset(5);
const ast = qb.toAST();
expect(ast.columns).toEqual(['id', 'name']);
expect(ast.where).toEqual({ age: { $gt: 18 } });
expect(ast.orderBy).toEqual([{ column: 'name', direction: 'asc' }]);
expect(ast.limit).toBe(10);
expect(ast.offset).toBe(5);
});
it('多次 where 合并条件', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.where({ age: { $gt: 18 } }).where({ active: true });
expect(qb.toAST().where).toEqual({ age: { $gt: 18 }, active: true });
});
it('多次 orderBy 追加排序', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.orderBy('age', 'desc').orderBy('name', 'asc');
expect(qb.toAST().orderBy).toHaveLength(2);
});
});
// ---- UpdateQueryBuilder ----
describe('UpdateQueryBuilder', () => {
it('基础更新', () => {
const qb = new UpdateQueryBuilder(engine, 'users', { age: 31 });
qb.where({ id: '1' });
const ast = qb.toAST();
expect(ast).toMatchObject({
type: 'UPDATE',
table: 'users',
sets: { age: 31 },
where: { id: '1' },
});
});
it('不带 where', () => {
const qb = new UpdateQueryBuilder(engine, 'users', { active: false });
expect(qb.toAST().where).toEqual({});
});
});
// ---- DeleteQueryBuilder ----
describe('DeleteQueryBuilder', () => {
it('基础删除', () => {
const qb = new DeleteQueryBuilder(engine, 'users');
qb.where({ id: '1' });
const ast = qb.toAST();
expect(ast).toMatchObject({
type: 'DELETE',
from: 'users',
where: { id: '1' },
});
});
it('不带 where', () => {
const qb = new DeleteQueryBuilder(engine, 'users');
expect(qb.toAST().where).toEqual({});
});
});
});
// ===================================================================
describe('Compiler', () => {
it('编译 SELECT', () => {
const ast = parse('SELECT id, name FROM users WHERE age > 18 ORDER BY age DESC LIMIT 10');
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.columns).toEqual(['id', 'name']);
expect(plan.where).toBeDefined();
expect(plan.orderBy).toEqual([{ column: 'age', direction: 'desc' }]);
expect(plan.limit).toBe(10);
});
it('编译 DELETE', () => {
const ast = parse("DELETE FROM users WHERE id = '1'");
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.where).toBeDefined();
});
it('编译 UPDATE', () => {
const ast = parse("UPDATE users SET age = 31 WHERE id = '1'");
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.where).toBeDefined();
});
it('非查询语句抛出错误', () => {
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY)');
expect(() => compileStatement(ast)).toThrow('Cannot compile');
});
});
// ===================================================================
describe('QueryExecutor', () => {
let engine: MemoryEngine;
let executor: QueryExecutor;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-exec', 1);
executor = new QueryExecutor(engine);
});
afterEach(async () => {
await engine.close();
});
it('执行 CREATE TABLE', async () => {
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)');
await executor.execute(ast);
expect(await engine.hasTable('users')).toBe(true);
});
it('执行 INSERT + SELECT', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
await executor.execute(parse("INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)"));
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob', 25)"));
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('执行 UPDATE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice', 30)"));
await executor.execute(parse("UPDATE users SET age = 31 WHERE id = '1'"));
const result = await executor.execute(parse("SELECT * FROM users WHERE id = '1'")) as Record<string, unknown>[];
expect(result[0].age).toBe(31);
});
it('执行 DELETE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)'));
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice')"));
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob')"));
await executor.execute(parse("DELETE FROM users WHERE id = '1'"));
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
expect(result).toHaveLength(1);
});
it('执行 DROP TABLE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY)'));
await executor.execute(parse('DROP TABLE users'));
expect(await engine.hasTable('users')).toBe(false);
});
it('操作不存在的表抛出错误', async () => {
await expect(
executor.execute(parse('SELECT * FROM nonexistent')),
).rejects.toThrow('does not exist');
});
it('getEngine', () => {
expect(executor.getEngine()).toBe(engine);
});
});
+96
View File
@@ -0,0 +1,96 @@
/**
* Lexer 边缘场景测试
*/
import { Lexer, tokenize } from '../../src/sql/lexer';
import { TokenType } from '../../src/sql/tokens';
describe('Lexer 边缘场景', () => {
it('空字符串返回 EOF', () => {
const tokens = tokenize('');
expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe(TokenType.EOF);
});
it('只包含空格', () => {
const tokens = tokenize(' \t\n ');
expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe(TokenType.EOF);
});
it('非法字符返回 ILLEGAL', () => {
const tokens = tokenize('@');
expect(tokens[0].type).toBe(TokenType.ILLEGAL);
});
it('完整的 CREATE TABLE 语句', () => {
const tokens = tokenize('CREATE TABLE users (id STRING PRIMARY KEY, name STRING NOT NULL, age NUMBER DEFAULT 0)');
const types = tokens.map(t => t.type);
expect(types).toEqual([
TokenType.CREATE, TokenType.TABLE, TokenType.IDENTIFIER, TokenType.LPAREN,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.PRIMARY, TokenType.KEY,
TokenType.COMMA,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.NOT, TokenType.NULL,
TokenType.COMMA,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.DEFAULT, TokenType.NUMBER,
TokenType.RPAREN, TokenType.EOF,
]);
});
it('带转义单引号的字符串', () => {
// SQL 中两个单引号 '' 表示转义的单引号
const tokens = tokenize("SELECT 'hello world'");
expect(tokens[1].type).toBe(TokenType.STRING);
expect(tokens[1].value).toBe('hello world');
});
it('双引号字符串', () => {
const tokens = tokenize('SELECT "hello world"');
expect(tokens[1].type).toBe(TokenType.STRING);
expect(tokens[1].value).toBe('hello world');
});
it('所有关键字', () => {
const sql = 'SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE DROP ORDER BY ASC DESC LIMIT OFFSET AND OR NOT LIKE IN PRIMARY KEY UNIQUE DEFAULT NULL TRUE FALSE';
const tokens = tokenize(sql);
const nonEof = tokens.filter(t => t.type !== TokenType.EOF);
expect(nonEof).toHaveLength(30);
// 验证都是关键字,不是 IDENTIFIER
for (const t of nonEof) {
expect(t.type).not.toBe(TokenType.IDENTIFIER);
}
});
it('下划线标识符', () => {
const tokens = tokenize('SELECT my_col_1 FROM user_table');
expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
expect(tokens[1].value).toBe('my_col_1');
expect(tokens[3].type).toBe(TokenType.IDENTIFIER);
expect(tokens[3].value).toBe('user_table');
});
it('运算符分隔符', () => {
const tokens = tokenize('= != > >= < <= <> ( ) , ; *');
const types = tokens.map(t => t.type);
expect(types).toEqual([
TokenType.EQ, TokenType.NEQ, TokenType.GT, TokenType.GTE,
TokenType.LT, TokenType.LTE, TokenType.NEQ,
TokenType.LPAREN, TokenType.RPAREN, TokenType.COMMA,
TokenType.SEMICOLON, TokenType.STAR, TokenType.EOF,
]);
});
it('数字: 整数和浮点数和负数', () => {
const tokens = tokenize('42 3.14 -7 -2.5');
expect(tokens[0].value).toBe('42');
expect(tokens[1].value).toBe('3.14');
expect(tokens[2].value).toBe('-7');
expect(tokens[3].value).toBe('-2.5');
});
it('Token position 正确', () => {
const lexer = new Lexer('SELECT * FROM users');
const tok = lexer.nextToken();
expect(tok.position).toBe(0); // SELECT 从位置 0 开始
});
});
+153
View File
@@ -0,0 +1,153 @@
/**
* Parser 边缘场景测试
*/
import { parse } from '../../src/sql/parser';
describe('Parser 边缘场景', () => {
// ---- SELECT 扩展 ----
describe('SELECT 扩展', () => {
it('WHERE 多条件 AND', () => {
const ast = parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'");
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
});
it('WHERE IN 列表', () => {
const ast = parse('SELECT * FROM users WHERE id IN (1, 2, 3)');
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
const cond = (ast.where as any).id;
expect(cond.$in).toEqual([1, 2, 3]);
});
it('WHERE IS NULL', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
});
it('WHERE IS NOT NULL', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NOT NULL');
expect(ast.type).toBe('SELECT');
});
it('WHERE NOT', () => {
const ast = parse("SELECT * FROM users WHERE NOT age = 18");
expect(ast.type).toBe('SELECT');
});
it('WHERE 括号分组', () => {
const ast = parse("SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE");
expect(ast.type).toBe('SELECT');
});
it('ORDER BY 多列', () => {
const ast = parse('SELECT * FROM users ORDER BY age DESC, name ASC');
expect(ast.type).toBe('SELECT');
expect(ast.orderBy).toHaveLength(2);
expect(ast.orderBy![0]).toEqual({ column: 'age', direction: 'desc' });
expect(ast.orderBy![1]).toEqual({ column: 'name', direction: 'asc' });
});
it('只有 LIMIT 没有 OFFSET', () => {
const ast = parse('SELECT * FROM users LIMIT 5');
expect(ast.limit).toBe(5);
expect(ast.offset).toBeUndefined();
});
it('只有 OFFSET 没有 LIMIT', () => {
const ast = parse('SELECT * FROM users OFFSET 10');
expect(ast).toMatchObject({ offset: 10 });
});
it('不带 WHERE 的 SELECT', () => {
const ast = parse('SELECT id, name FROM users ORDER BY id LIMIT 5');
expect(ast.where).toEqual({});
});
});
// ---- INSERT 扩展 ----
describe('INSERT 扩展', () => {
it('不带列名的 INSERT', () => {
const ast = parse("INSERT INTO users VALUES ('1', 'Alice', 30)");
expect(ast.columns).toBeUndefined();
expect(ast.values).toEqual([['1', 'Alice', 30]]);
});
it('INSERT 布尔值和 NULL', () => {
const ast = parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)');
expect(ast.values[0]).toEqual([true, false, null]);
});
it('INSERT 负数和浮点数', () => {
const ast = parse('INSERT INTO scores VALUES (-1, 3.14)');
expect(ast.values[0]).toEqual([-1, 3.14]);
});
});
// ---- UPDATE 扩展 ----
describe('UPDATE 扩展', () => {
it('UPDATE 多列', () => {
const ast = parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'");
expect(ast.type).toBe('UPDATE');
expect(ast.sets).toEqual({ name: 'Bob', age: 26 });
});
it('UPDATE 不带 WHERE', () => {
const ast = parse('UPDATE users SET active = FALSE');
expect(ast.where).toEqual({});
});
});
// ---- DELETE 扩展 ----
describe('DELETE 扩展', () => {
it('DELETE 不带 WHERE', () => {
const ast = parse('DELETE FROM users');
expect(ast.where).toEqual({});
});
it('DELETE 带复杂 WHERE', () => {
const ast = parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'");
expect(ast.type).toBe('DELETE');
});
});
// ---- DDL 扩展 ----
describe('DDL 扩展', () => {
it('CREATE TABLE 完整修饰符', () => {
const ast = parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)");
expect(ast.type).toBe('CREATE_TABLE');
expect(ast.columns).toHaveLength(4);
expect(ast.columns[0]).toMatchObject({ name: 'id', primaryKey: true });
expect(ast.columns[1]).toMatchObject({ name: 'name', required: true, unique: true });
expect(ast.columns[2]).toMatchObject({ name: 'price', default: 0 });
expect(ast.columns[3]).toMatchObject({ name: 'active', default: true });
});
it('不支持 IF EXISTS 语法(表名被解析为 IF', () => {
// 解析器把 IF 当作表名,EXISTS 作为后续 token 导致错误
const ast = parse('DROP TABLE users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' });
});
});
// ---- 值类型 ----
describe('常量值解析', () => {
it('布尔值 TRUE/FALSE', () => {
const ast = parse("SELECT * FROM users WHERE active = TRUE");
expect(ast.where).toBeDefined();
});
it('NULL 值', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
expect(ast.where).toBeDefined();
});
});
});
+165
View File
@@ -0,0 +1,165 @@
/**
* Schema 边缘场景测试
*/
import { createSchema, validateRow, getPrimaryKey, checkFieldType, astColumnToColumnDef } from '../../src/table/schema';
describe('Schema 边缘场景', () => {
// ---- createSchema ----
describe('createSchema', () => {
it('空列表抛出错误', () => {
expect(() => createSchema('empty', {})).toThrow('at least one column');
});
it('多主键定义(取第一个标记的)', () => {
const schema = createSchema('multi', {
id1: { type: 'string', primaryKey: true },
id2: { type: 'string' },
});
expect(getPrimaryKey(schema)).toBe('id1');
});
it('大表定义', () => {
const cols: Record<string, any> = {};
for (let i = 0; i < 50; i++) {
cols[`col${i}`] = { type: 'string' };
}
cols.id = { type: 'string', primaryKey: true };
const schema = createSchema('big', cols);
expect(Object.keys(schema.columns)).toHaveLength(51);
});
});
// ---- getPrimaryKey ----
describe('getPrimaryKey', () => {
it('没有标记主键时返回第一列', () => {
const schema = createSchema('test', {
uuid: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 重新构造不带主键标记的 schema
const noPkSchema = { name: 'test', columns: {
first: { type: 'string' as const },
second: { type: 'string' as const },
}};
expect(getPrimaryKey(noPkSchema)).toBe('first');
});
});
// ---- validateRow ----
describe('validateRow', () => {
const schema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
bio: { type: 'string', maxLength: 200 },
score: { type: 'number', min: 0, max: 100, default: 50 },
active: { type: 'boolean', default: true },
birthday: { type: 'date' },
tags: { type: 'json', default: [] },
});
it('跳过 undefined 字段(使用默认值)', () => {
const row = validateRow(schema, { id: '1', name: 'Alice' });
expect(row.id).toBe('1');
expect(row.name).toBe('Alice');
expect(row.score).toBe(50);
expect(row.active).toBe(true);
expect(row.tags).toEqual([]);
});
it('null 值被视为不满足必填', () => {
expect(() => validateRow(schema, { id: '1', name: null })).toThrow('required');
});
it('json 类型接受对象', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: { key: 'value' } });
expect(row.tags).toEqual({ key: 'value' });
});
it('json 类型接受数组', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: [1, 2, 3] });
expect(row.tags).toEqual([1, 2, 3]);
});
it('date 类型接受有效日期字符串', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', birthday: '2024-01-15' });
expect(row.birthday).toBe('2024-01-15');
});
it('date 类型拒绝无效格式', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 'not-a-date' }))
.toThrow('expects valid date');
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 123 }))
.toThrow('expects valid date');
});
it('boolean 类型', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', active: false });
expect(row.active).toBe(false);
});
it('boolean 拒绝非布尔值', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', active: 'yes' }))
.toThrow('expects boolean');
});
it('number 拒绝字符串', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', score: 'high' }))
.toThrow('expects number');
});
it('string 拒绝数字', () => {
expect(() => validateRow(schema, { id: 123, name: 'Alice' }))
.toThrow('expects string');
});
});
// ---- checkFieldType ----
describe('checkFieldType', () => {
it('string 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'string', 'hello')).not.toThrow();
});
it('string 类型拒绝', () => {
expect(() => checkFieldType('t', 'c', 'string', 123)).toThrow('expects string');
});
it('number 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'number', 42)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 3.14)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 0)).not.toThrow();
});
it('boolean 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'boolean', true)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'boolean', false)).not.toThrow();
});
it('json 拒绝非对象', () => {
expect(() => checkFieldType('t', 'c', 'json', 'not-object')).toThrow('expects object');
expect(() => checkFieldType('t', 'c', 'json', 123)).toThrow('expects object');
});
});
// ---- astColumnToColumnDef ----
describe('astColumnToColumnDef', () => {
it('转换基础列', () => {
const result = astColumnToColumnDef({ name: 'id', type: 'string' });
expect(result.type).toBe('string');
});
it('转换带修饰符的列', () => {
const result = astColumnToColumnDef({
name: 'id', type: 'string', primaryKey: true, unique: true, required: true,
});
expect(result.primaryKey).toBe(true);
expect(result.unique).toBe(true);
expect(result.required).toBe(true);
});
});
});
+79
View File
@@ -0,0 +1,79 @@
/**
* Transaction 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { TransactionManager, Transaction } from '../../src/transaction/index';
import { createSchema } from '../../src/table/schema';
describe('Transaction', () => {
let engine: MemoryEngine;
let tm: TransactionManager;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
});
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-tx', 1);
await engine.createTable(userSchema);
tm = new TransactionManager(engine);
});
afterEach(async () => {
await engine.close();
});
it('事务中执行插入', async () => {
await tm.execute(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
expect(await engine.count('users')).toBe(2);
});
it('事务中执行混合操作', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', balance: 100 }]);
await tm.execute(async (trx) => {
await trx.table('users').update({ balance: 200 }).where({ id: '1' }).execute();
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].balance).toBe(200);
expect(await engine.count('users')).toBe(2);
});
it('事务中的 table() 返回相同实例', async () => {
await tm.execute(async (trx) => {
const t1 = trx.table('users');
const t2 = trx.table('users');
expect(t1).toBe(t2); // 相同表名返回同一实例
});
});
it('Transaction.isCompleted', async () => {
let trxRef: Transaction | null = null;
await tm.execute(async (trx) => {
trxRef = trx;
expect(trx.isCompleted()).toBe(false);
await trx.table('users').insert({ id: '1', name: 'Alice' });
});
expect(trxRef!.isCompleted()).toBe(true);
});
it('事务抛出错误', async () => {
await expect(tm.execute(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice' });
throw new Error('intentional error');
})).rejects.toThrow('Transaction failed');
// 注意:MemoryEngine 没有回滚,所以数据已经写入
// v0.0.1 的 Transaction 只是错误包装
});
});
+99
View File
@@ -0,0 +1,99 @@
/**
* utils.ts
*/
import {
generateId,
escapeHTML,
debounce,
throttle,
deepMerge,
isBrowser,
} from '../src/utils';
describe('generateId', () => {
test('返回字符串', () => {
expect(typeof generateId()).toBe('string');
});
test('多次调用产生不同值', () => {
const ids = new Set<string>();
for (let i = 0; i < 100; i++) ids.add(generateId());
expect(ids.size).toBe(100);
});
});
describe('escapeHTML', () => {
test('转义 < > &', () => {
const out = escapeHTML('<a>&b</a>');
expect(out).toContain('&lt;');
expect(out).toContain('&gt;');
expect(out).toContain('&amp;');
});
test('null/undefined 返回空字符串', () => {
expect(escapeHTML(null)).toBe('');
expect(escapeHTML(undefined)).toBe('');
});
});
describe('debounce', () => {
test('延迟执行', (done) => {
let called = 0;
const fn = debounce(() => { called++; }, 50);
fn();
expect(called).toBe(0);
setTimeout(() => {
expect(called).toBe(1);
done();
}, 100);
});
test('多次调用只执行最后一次', (done) => {
let result = 0;
const fn = debounce((v: number) => { result = v; }, 50);
fn(1);
fn(2);
fn(3);
setTimeout(() => {
expect(result).toBe(3);
done();
}, 100);
});
});
describe('throttle', () => {
test('首次立即执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
expect(called).toBe(1);
});
test('限流期内不重复执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
fn();
fn();
expect(called).toBe(1);
});
});
describe('deepMerge', () => {
test('浅层合并', () => {
const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 });
expect(out).toEqual({ a: 1, b: 3, c: 4 });
});
test('深层对象合并', () => {
const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } });
expect(out).toEqual({ obj: { x: 1, y: 2 } });
});
});
describe('isBrowser', () => {
test('jsdom 环境下返回 true', () => {
expect(isBrowser()).toBe(true);
});
});
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}