release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 10m8s
CI / test (24.x) (push) Successful in 9m55s
CI / test (18.x) (push) Successful in 10m9s

新增:
- AriaEngine 外键级联(CASCADE/SET NULL/RESTRICT)+ clearAll() 重置 API
- 引擎级 alterTable:Aria DROP COLUMN 重写存储行 + schema 持久化
- 流式查询 queryStream / findStream(LSM 惰性扫描不物化)
- FROM 派生表 / 多列 ON 哈希连接 / COUNT(DISTINCT) / NULLS FIRST/LAST
- 普通列别名 + ORDER BY 别名 + 无表查询 + 字符串常量列
- 演示页引擎切换器(Memory/Aria)+ 预设自动重置

修复:
- Aria WAL DROP_TABLE 崩溃恢复(删表复活)+ 恢复后 WAL 截断
- Memory update/delete 索引维护(unique 约束绕过)
- 关联 EXISTS 绑定失效 / HAVING 标量子查询 / INSERT SELECT 位置错位
- 裸布尔列条件(WHERE done / CASE WHEN done)
- Aria $in 重复行 / JOIN 主表 WHERE 下推 / DROP INDEX 报错
- ORDER BY/GROUP BY/SELECT 表前缀列 + SQL '' 标准转义

质量:894 测试 · 47 套件 · 81.5% 覆盖率
This commit is contained in:
thzxx
2026-08-08 13:36:34 +08:00
parent 82ad8e93b9
commit a1e4f5071c
38 changed files with 13493 additions and 8756 deletions
+121 -54
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧪 在线演示 — MetonaSqlark v0.3.2</title>
<title>🧪 在线演示 — MetonaSqlark v0.4.1</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
@@ -84,13 +84,15 @@
<a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a>
</nav>
<div class="status"><span class="dot"></span> Memory 模式 — v0.3.2</div>
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.4.1</div>
<button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button>
<button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</header>
<div class="main">
<div class="editor-panel">
<div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.3.2 在线演示
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.4.1 在线演示
-- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
@@ -128,6 +130,7 @@
<button class="btn btn-preset" onclick="loadPreset('exists')">🔍 EXISTS</button>
<button class="btn btn-preset" onclick="loadPreset('index')">🗂 索引</button>
<button class="btn btn-preset" onclick="loadPreset('multistmt')">📜 多语句/事务</button>
<button class="btn btn-preset" onclick="loadPreset('v040')" style="color:#22c55e;border-color:#22c55e;">🚰 v0.4.0 新特性</button>
<button class="btn btn-preset" onclick="loadPreset('aria')" style="color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</div>
</div>
@@ -161,57 +164,84 @@ const rowCount = document.getElementById('row-count');
const sqlInput = document.getElementById('sql-input');
// Init database
let currentEngine = 'memory';
async function initDB() {
try {
db = new DBClass({ name: 'demo', mode: 'memory' });
const engine = currentEngine;
// 关闭旧实例(预设重置/引擎切换时数据回到初始状态)
if (db) { try { await db.close(); } catch { /* ignore */ } }
db = new DBClass({ name: 'demo', mode: engine });
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' },
quantity: { type: 'number', default: 1 },
});
await db.defineTable('products', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
price: { type: 'number', default: 0 },
category: { type: 'string' },
});
// v0.4.1: Aria 引擎持久化 — 每次加载清空上次演示数据,保证演示确定性
if (engine === 'aria' && typeof db.getEngine().clearAll === 'function') {
await db.getEngine().clearAll();
}
// Seed data
await db.table('users').insertMany([
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
{ id: '4', name: 'Diana', email: 'diana@demo.com', age: 28 },
{ id: '5', name: 'Eve', email: 'eve@demo.com', age: 22 },
]);
await db.table('orders').insertMany([
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200, quantity: 1 },
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50, quantity: 2 },
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150, quantity: 1 },
{ id: 'o4', user_id: '3', product: 'Monitor', amount: 400, quantity: 1 },
{ id: 'o5', user_id: '3', product: 'Cable', amount: 20, quantity: 3 },
]);
await db.table('products').insertMany([
{ id: 'p1', name: 'Laptop', price: 1200, category: 'Electronics' },
{ id: 'p2', name: 'Mouse', price: 50, category: 'Accessories' },
{ id: 'p3', name: 'Keyboard', price: 150, category: 'Accessories' },
]);
await seedDemoData(db);
renderInfo(`${engine === 'aria' ? '🌲 AriaEngineLSM-Tree + WAL + MVCC' : '⚡ Memory'} 引擎已就绪`);
} catch (e) {
renderError('数据库初始化失败: ' + e.message);
}
}
// 建表 + 种子数据(每次预设重置/引擎切换后重建,保证演示可预期)
async function seedDemoData(db) {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
email: { type: 'string' },
age: { type: 'number', default: 0 },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
product: { type: 'string' },
amount: { type: 'number' },
quantity: { type: 'number', default: 1 },
});
await db.defineTable('products', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
price: { type: 'number', default: 0 },
category: { type: 'string' },
});
// Seed data
await db.table('users').insertMany([
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
{ id: '4', name: 'Diana', email: 'diana@demo.com', age: 28 },
{ id: '5', name: 'Eve', email: 'eve@demo.com', age: 22 },
]);
await db.table('orders').insertMany([
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200, quantity: 1 },
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50, quantity: 2 },
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150, quantity: 1 },
{ id: 'o4', user_id: '3', product: 'Monitor', amount: 400, quantity: 1 },
{ id: 'o5', user_id: '3', product: 'Cable', amount: 20, quantity: 3 },
]);
await db.table('products').insertMany([
{ id: 'p1', name: 'Laptop', price: 1200, category: 'Electronics' },
{ id: 'p2', name: 'Mouse', price: 50, category: 'Accessories' },
{ id: 'p3', name: 'Keyboard', price: 150, category: 'Accessories' },
]);
}
// v0.4.1: 切换存储引擎(重建数据库实例)
async function switchEngine(engine) {
if (engine === currentEngine) return;
currentEngine = engine;
document.getElementById('btn-memory').style.opacity = engine === 'memory' ? '1' : '0.6';
document.getElementById('btn-aria').style.opacity = engine === 'aria' ? '1' : '0.6';
document.getElementById('engine-status').textContent = engine === 'aria' ? '🌲 Aria' : '⚡ Memory';
document.getElementById('engine-dot').style.background = engine === 'aria' ? '#ec4899' : '';
renderInfo(`⏳ 正在初始化 ${engine === 'aria' ? 'Aria' : 'Memory'} 引擎…`);
await initDB();
}
// Run SQL
async function runQuery() {
const sql = sqlInput.value.trim();
@@ -316,6 +346,10 @@ function renderSuccess(msg) {
resultArea.innerHTML = `<div class="success-box">${msg}</div>`;
}
function renderInfo(msg) {
resultArea.innerHTML = `<div class="success-box" style="color:#22c55e;border-color:#22c55e;">${msg}</div>`;
}
function clearResults() {
resultArea.innerHTML = `<div class="empty-state"><div class="icon">⚡</div><div>结果已清空</div></div>`;
updateRowCount(0);
@@ -518,11 +552,13 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理
DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.2.5 → v0.3.2)
-- AriaEngine: LSM-Tree 自研存储引擎
-- 支持 LSM-Tree · WAL CRC同步 · MVCC版本链 · BloomFilter · 二级索引 · AES-GCM · 836测试
aria: `-- 🌲 AriaEngine 演示 (v0.4.1)
-- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine
-- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter
-- 基础 CRUD 与 Memory 引擎完全兼容
-- 基础 CRUD 完全兼容
-- 当前引擎确认
-- (内存中 Aria 引擎实例,数据经 WAL + SSTable 持久化到 IndexedDB)
CREATE TABLE IF NOT EXISTS tasks (
id STRING PRIMARY KEY,
title STRING NOT NULL,
@@ -556,8 +592,9 @@ DROP TABLE temp_logs;`,
-- • Buffer Pool: SSTable LRU 缓存 (256页 ~ 1MB) ✅ 已生效
-- • Bloom Filter: FNV-1a + Murmur 双哈希
-- • 二级索引: 每列独立 LSM + 动态 CREATE INDEX
-- • 外键级联: CASCADE / SET NULL / RESTRICT (v0.4.1)
-- 生产环境: mode: 'aria' 激活自研引擎
-- 生产环境 API(与演示页右上角切换等价)
-- const db = await MetonaSqlark.create({
-- name: 'my-app', mode: 'aria'
-- });`,
@@ -634,10 +671,10 @@ JOIN orders o ON u.id = o.user_id
WHERE EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.id AND o2.amount > 100);`,
index: `-- 🗂 动态索引 CREATE / DROP INDEX (v0.3.0)
-- 为 orders.user_id 创建索引
-- 为 orders.user_id 创建索引(已有数据自动回填)
CREATE INDEX idx_orders_user ON orders (user_id);
-- 索引查找(走二级索引)
-- 索引查找(v0.4.1: JOIN 主表 WHERE 条件下推到引擎,真正走二级索引)
SELECT u.name, o.product, o.amount
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.user_id = '1';
@@ -646,7 +683,10 @@ WHERE o.user_id = '1';
DROP INDEX idx_orders_user ON orders (user_id);
-- 删除后回退全表扫描(结果不变)
SELECT * FROM orders WHERE user_id = '3';`,
SELECT * FROM orders WHERE user_id = '3';
-- DROP 不存在的索引会报错(INDEX_NOT_FOUND
-- DROP INDEX idx_nonexist ON orders (user_id);`,
multistmt: `-- 📜 多语句 + 事务语句 (v0.3.0)
-- 分号分隔的多语句一次执行
@@ -667,11 +707,35 @@ BEGIN;
INSERT INTO audit VALUES ('a4', 'will be committed');
COMMIT;
SELECT COUNT(*) as total FROM audit;`,
v040: `-- 🚰 v0.4.0 新特性
-- 派生表:FROM (SELECT ...) 子查询作为行源
SELECT category, total FROM (
SELECT category, SUM(price) AS total FROM products GROUP BY category
) AS t WHERE total > 100 ORDER BY total DESC;
-- COUNT(DISTINCT col):去重计数
SELECT COUNT(DISTINCT category) AS categories FROM products;
-- NULLS FIRST / LASTNULL 排序位置控制
SELECT name FROM users ORDER BY age ASC NULLS FIRST LIMIT 3;
-- 普通列别名 + ORDER BY 别名
SELECT name AS n FROM users ORDER BY n DESC;
-- SQL 标准 '' 字符串转义
SELECT 'it''s a test' AS escaped;
-- 哈希连接(等值 ON 走 O(N+M);多列等值 ON 同样支持)
SELECT u.name, o.product FROM users u
INNER JOIN orders o ON o.user_id = u.id;`,
};
function loadPreset(name) {
// v0.4.1: 点击预设前自动重置数据(前序预设可能修改/删除种子数据,重置保证每个预设可预期演示)
async function loadPreset(name) {
if (presets[name]) {
sqlInput.value = presets[name];
await initDB();
runQuery();
}
}
@@ -685,8 +749,11 @@ document.addEventListener('keydown', e => {
});
// Boot
document.getElementById('btn-memory').style.opacity = '1';
document.getElementById('btn-aria').style.opacity = '0.6';
document.getElementById('engine-status').textContent = '⚡ Memory';
initDB().then(() => {
console.log('✅ MetonaSqlark v0.3.2 demo ready');
console.log('✅ MetonaSqlark v0.4.1 demo ready');
setTimeout(runQuery, 300);
}).catch(err => {
renderError('初始化失败: ' + err.message);