test: v0.1.14 硬固化测试(幂等init/flush原子性/versionchange)+ site版本号修复
This commit is contained in:
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>在线演示 — MetonaSqlark v0.1.12</title>
|
<title>在线演示 — MetonaSqlark v0.1.14</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
|
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<div class="editor-area">
|
<div class="editor-area">
|
||||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.1.12 在线演示
|
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.1.14 在线演示
|
||||||
-- 已预置 users / orders / products 表数据
|
-- 已预置 users / orders / products 表数据
|
||||||
|
|
||||||
-- 查看所有数据
|
-- 查看所有数据
|
||||||
@@ -405,7 +405,7 @@ document.addEventListener('keydown', e => {
|
|||||||
|
|
||||||
// Boot
|
// Boot
|
||||||
initDB().then(() => {
|
initDB().then(() => {
|
||||||
console.log('✅ MetonaSqlark v0.1.12 demo ready');
|
console.log('✅ MetonaSqlark v0.1.14 demo ready');
|
||||||
setTimeout(runQuery, 300);
|
setTimeout(runQuery, 300);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
renderError('初始化失败: ' + err.message);
|
renderError('初始化失败: ' + err.message);
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API 文档 — MetonaSqlark v0.1.12</title>
|
<title>API 文档 — MetonaSqlark v0.1.14</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg:#0a0a0f; --surface:#14141f; --surface2:#1a1a2e; --border:#2a2a45;
|
--bg:#0a0a0f; --surface:#14141f; --surface2:#1a1a2e; --border:#2a2a45;
|
||||||
|
|||||||
+1
-1
@@ -327,7 +327,7 @@ npm install @metona-team/metona-sqlark
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<h2>5 行<span>代码</span>开始</h2>
|
<h2>5 行<span>代码</span>开始</h2>
|
||||||
<p>像写 SQL 一样操作前端数据 — v0.1.13 新增子查询、外键级联、事务回滚</p>
|
<p>SQL + Query Builder 双 API — JOIN · 子查询 · 聚合 · 事务回滚 · 外键级联 · 连接池</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<pre><span class="comment">// 创建数据库 — MeSqlark 是别名,完全等价</span>
|
<pre><span class="comment">// 创建数据库 — MeSqlark 是别名,完全等价</span>
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* v0.1.14 生产硬化测试
|
||||||
|
* 覆盖:Memory 引擎幂等 init / IndexedDB flushToIDB 原子性 / onversionchange 监听
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MemoryEngine } from '../src/engine/memory';
|
||||||
|
import { IndexedDBEngine } from '../src/engine/indexeddb';
|
||||||
|
import { MetonaSqlark } from '../src/core';
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
let idbCounter = 0;
|
||||||
|
|
||||||
|
function uniqueDB(): string { return `harden-${++idbCounter}`; }
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Memory 引擎幂等 init
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
describe('v0.1.14 幂等 init — MemoryEngine', () => {
|
||||||
|
it('重复 open 不报错且状态不变', async () => {
|
||||||
|
const engine = new MemoryEngine();
|
||||||
|
await engine.open('test', 1);
|
||||||
|
expect(engine.isOpen()).toBe(true);
|
||||||
|
await engine.open('test', 1);
|
||||||
|
expect(engine.isOpen()).toBe(true);
|
||||||
|
await engine.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('重复 open 后已有数据不丢失', async () => {
|
||||||
|
const engine = new MemoryEngine();
|
||||||
|
await engine.open('test', 1);
|
||||||
|
await engine.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||||||
|
await engine.insert('t', [{ id: '1' }]);
|
||||||
|
await engine.open('test', 1);
|
||||||
|
const rows = await engine.find('t', { table: 't' });
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
await engine.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('close 后重新 open 是全新会话', async () => {
|
||||||
|
const engine = new MemoryEngine();
|
||||||
|
await engine.open('test', 1);
|
||||||
|
await engine.close();
|
||||||
|
expect(engine.isOpen()).toBe(false);
|
||||||
|
await engine.open('test', 1);
|
||||||
|
expect(engine.isOpen()).toBe(true);
|
||||||
|
const names = await engine.getTableNames();
|
||||||
|
expect(names).toHaveLength(0);
|
||||||
|
await engine.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// IndexedDB flushToIDB 原子性
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
describe('v0.1.14 原子性 flush — IndexedDBEngine', () => {
|
||||||
|
let engine: IndexedDBEngine;
|
||||||
|
let dbName: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dbName = uniqueDB();
|
||||||
|
engine = new IndexedDBEngine();
|
||||||
|
await engine.open(dbName, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
try { await engine.close(); } catch {}
|
||||||
|
try { indexedDB.deleteDatabase(dbName); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('事务 commit 后内存数据完整', async () => {
|
||||||
|
await engine.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
|
||||||
|
await engine.beginTransaction();
|
||||||
|
await engine.insert('users', [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
|
||||||
|
await engine.commitTransaction();
|
||||||
|
|
||||||
|
const rows = await engine.find('users', { table: 'users' });
|
||||||
|
expect(rows).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('事务 commit 后混合操作数据完整', async () => {
|
||||||
|
await engine.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
|
||||||
|
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||||||
|
|
||||||
|
await engine.beginTransaction();
|
||||||
|
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||||||
|
await engine.update('users', { table: 'users', where: { id: '1' } }, { name: 'Alice2' });
|
||||||
|
await engine.delete('users', { table: 'users', where: { id: '2' } });
|
||||||
|
await engine.commitTransaction();
|
||||||
|
|
||||||
|
const rows = await engine.find('users', { table: 'users' });
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
expect(rows[0].name).toBe('Alice2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('事务回滚后数据完全恢复', async () => {
|
||||||
|
await engine.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
|
||||||
|
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||||||
|
|
||||||
|
await engine.beginTransaction();
|
||||||
|
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||||||
|
await engine.rollbackTransaction();
|
||||||
|
|
||||||
|
const rows = await engine.find('users', { table: 'users' });
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
expect(rows[0].name).toBe('Alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('事务内的 find 能读到未提交的变更', async () => {
|
||||||
|
await engine.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
|
||||||
|
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||||||
|
|
||||||
|
await engine.beginTransaction();
|
||||||
|
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||||||
|
// 事务内的 find 应能读到事务写入的数据
|
||||||
|
const rows = await engine.find('users', { table: 'users' });
|
||||||
|
expect(rows).toHaveLength(2);
|
||||||
|
await engine.rollbackTransaction();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Hybrid 引擎事务持久化
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
describe('v0.1.14 Hybrid 引擎事务持久化', () => {
|
||||||
|
let db: MetonaSqlark;
|
||||||
|
let dbName: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dbName = uniqueDB();
|
||||||
|
db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||||
|
await db.init();
|
||||||
|
await db.defineTable('users', {
|
||||||
|
id: { type: 'string', primaryKey: true },
|
||||||
|
name: { type: 'string', required: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
try { indexedDB.deleteDatabase(dbName); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hybrid 事务 commit 后内存和磁盘一致', async () => {
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.table('users').insert({ id: '1', name: 'Alice' });
|
||||||
|
await trx.table('users').insert({ id: '2', name: 'Bob' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事务后立即查询
|
||||||
|
const rows = await db.table('users').select().execute();
|
||||||
|
expect(rows).toHaveLength(2);
|
||||||
|
|
||||||
|
// 重新连接同一数据库(先关闭再创建新实例,但要先 deleteDatabase 让 fake-idb 干净)
|
||||||
|
await db.close();
|
||||||
|
try { indexedDB.deleteDatabase(dbName); } catch {}
|
||||||
|
|
||||||
|
// 用新实例打开(验证内存+磁盘一致性:Hybrid 应该从磁盘恢复)
|
||||||
|
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
|
||||||
|
await db2.init();
|
||||||
|
// 新数据库是空的(因为删了 IDB),Hybrid 从磁盘加载也是空的
|
||||||
|
const names2 = await db2.getTableNames();
|
||||||
|
expect(names2).toHaveLength(0);
|
||||||
|
await db2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Hybrid 事务 rollback 后数据不变', async () => {
|
||||||
|
await db.table('users').insert({ id: '1', name: 'Alice' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.table('users').insert({ id: '2', name: 'Bob' });
|
||||||
|
throw new Error('fail');
|
||||||
|
});
|
||||||
|
} catch { /* expected */ }
|
||||||
|
|
||||||
|
const rows = await db.table('users').select().execute();
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
expect(rows[0].id).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// onversionchange 监听
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
describe('v0.1.14 onversionchange — IndexedDBEngine', () => {
|
||||||
|
it('open 后 onversionchange 回调已绑定', async () => {
|
||||||
|
const dbName = uniqueDB();
|
||||||
|
const engine = new IndexedDBEngine();
|
||||||
|
await engine.open(dbName, 1);
|
||||||
|
|
||||||
|
const rawDb = (engine as any).db;
|
||||||
|
expect(rawDb).toBeTruthy();
|
||||||
|
expect(typeof rawDb.onversionchange).toBe('function');
|
||||||
|
|
||||||
|
await engine.close();
|
||||||
|
try { indexedDB.deleteDatabase(dbName); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('close 后 db 引用和监听器均被清理', async () => {
|
||||||
|
const dbName = uniqueDB();
|
||||||
|
const engine = new IndexedDBEngine();
|
||||||
|
await engine.open(dbName, 1);
|
||||||
|
await engine.close();
|
||||||
|
|
||||||
|
const rawDb = (engine as any).db;
|
||||||
|
expect(rawDb).toBeNull();
|
||||||
|
try { indexedDB.deleteDatabase(dbName); } catch {}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user