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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 开始
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 只是错误包装
|
||||
});
|
||||
});
|
||||
@@ -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('<');
|
||||
expect(out).toContain('>');
|
||||
expect(out).toContain('&');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user