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

- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid
- 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT
- Query Builder链式API + TypeScript泛型支持
- 聚合函数:COUNT/SUM/AVG/MIN/MAX
- 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出
- React/Vue框架集成
- 264个测试用例,93.46%覆盖率
- 零运行时依赖
This commit is contained in:
thzxx
2026-07-26 15:00:01 +08:00
commit e2a590c5b1
60 changed files with 16359 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
/**
* Core 全覆盖测试 — 导入导出、迁移、发布订阅、钩子
*/
import { MetonaSqlark } from '../src/core';
describe('Core 全覆盖', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-coverage', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('users').insertMany([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
});
afterEach(async () => { await db.close(); });
// ---- 导入导出 ----
describe('导入导出', () => {
it('exportTable 导出单表', async () => {
const data = await db.exportTable('users');
expect(data).toHaveLength(2);
expect(data[0].name).toBe('Alice');
});
it('exportAll 导出全库', async () => {
const all = await db.exportAll();
expect(all.users).toHaveLength(2);
});
it('importTable 导入数据', async () => {
await db.defineTable('temp', { id: { type: 'string', primaryKey: true }, val: { type: 'number' } });
await db.importTable('temp', [{ id: 'a', val: 1 }, { id: 'b', val: 2 }]);
expect(await db.table('temp').count()).toBe(2);
});
});
// ---- 发布订阅 ----
describe('发布订阅', () => {
it('subscribe 注册监听', () => {
const fn = jest.fn();
const unsub = db.subscribe('users', fn);
expect(typeof unsub).toBe('function');
});
it('unsubscribe 取消监听', () => {
const fn = jest.fn();
const unsub = db.subscribe('users', fn);
unsub();
db.emit('users', { type: 'insert', row: { id: '3' } });
expect(fn).not.toHaveBeenCalled();
});
it('emit 触发监听', () => {
const fn = jest.fn();
db.subscribe('users', fn);
db.emit('users', { type: 'insert', row: { id: '3', name: 'Charlie' } });
expect(fn).toHaveBeenCalledWith({ type: 'insert', row: { id: '3', name: 'Charlie' } });
});
});
// ---- 迁移 ----
describe('迁移系统', () => {
it('addMigration + migrateTo', async () => {
let migrated = false;
db.addMigration(2, async (d) => {
migrated = true;
await d.defineTable('new_table', { id: { type: 'string', primaryKey: true } });
});
await db.migrateTo(2);
expect(migrated).toBe(true);
const tables = await db.getTableNames();
expect(tables).toContain('new_table');
});
it('不执行已过期的迁移', async () => {
let count = 0;
db.addMigration(1, async () => { count++; }); // version <= current
db.addMigration(2, async () => { count++; });
await db.migrateTo(2);
// version 1 <= current version 1, should be skipped
expect(count).toBe(1);
});
});
// ---- 钩子 ----
describe('钩子系统', () => {
it('on 注册钩子 (通过 pluginManager)', () => {
const fn = jest.fn();
db.getPluginManager().on('beforeInsert', fn);
db.getPluginManager().trigger('beforeInsert', { id: '3' });
expect(fn).toHaveBeenCalled();
});
it('getPluginManager 返回插件管理器', () => {
const pm = db.getPluginManager();
expect(pm).toBeDefined();
expect(typeof pm.on).toBe('function');
});
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('close 关闭数据库', async () => {
await db.close();
expect(db.isReady()).toBe(false);
});
it('getEngine 返回引擎', () => {
const engine = db.getEngine();
expect(engine).toBeDefined();
expect(engine.name).toBe('memory');
});
it('未初始化时操作抛出错误', () => {
const db2 = new MetonaSqlark({ name: 'x', mode: 'memory' });
expect(() => db2.table('x')).toThrow('not initialized');
});
});
// ---- 别名 ----
describe('MeSqlark 别名', () => {
it('MeSqlark === MetonaSqlark', () => {
const { MeSqlark } = require('../src/index');
expect(MeSqlark).toBe(MetonaSqlark);
});
});
});
+293
View File
@@ -0,0 +1,293 @@
/**
* MetonaSqlark 核心集成测试
*/
import { MetonaSqlark } from '../src/core';
import 'fake-indexeddb/auto';
let diskCounter = 0;
// ===================================================================
// Memory 模式
// ===================================================================
describe('MetonaSqlark Memory 模式', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-db-mem', mode: 'memory' });
await db.init();
});
afterEach(async () => {
await db.close();
});
it('创建数据库并初始化', () => {
expect(db.isReady()).toBe(true);
expect(db.name).toBe('test-db-mem');
expect(db.mode).toBe('memory');
});
it('定义表', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
});
const tables = await db.getTableNames();
expect(tables).toContain('users');
});
describe('Query Builder API', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
});
});
it('insert + select', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
await table.insert({ id: '2', name: 'Bob', age: 25 });
const results = await table.select().where({ age: { $gt: 20 } }).execute();
expect(results).toHaveLength(2);
});
it('insertMany', async () => {
const table = db.table('users');
await table.insertMany([
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
const count = await table.count();
expect(count).toBe(2);
});
it('update', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
const count = await table.update({ age: 31 }).where({ id: '1' }).execute();
expect(count).toBe(1);
const results = await table.select().where({ id: '1' }).execute();
expect(results[0].age).toBe(31);
});
it('delete', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice', age: 30 });
const count = await table.delete().where({ id: '1' }).execute();
expect(count).toBe(1);
expect(await table.count()).toBe(0);
});
it('orderBy + limit + offset', async () => {
const table = db.table('users');
await table.insertMany([
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Charlie', age: 35 },
]);
const results = await table.select().orderBy('age', 'desc').limit(2).offset(1).execute();
expect(results).toHaveLength(2);
});
it('dropTable', async () => {
await db.dropTable('users');
const tables = await db.getTableNames();
expect(tables).not.toContain('users');
});
});
describe('SQL API', () => {
beforeEach(async () => {
await db.query('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)');
});
it('SQL INSERT + SELECT', async () => {
await db.query("INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('SQL SELECT WHERE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
const result = await db.query("SELECT * FROM users WHERE age > 28") as Record<string, unknown>[];
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
it('SQL UPDATE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("UPDATE users SET age = 31 WHERE id = '1'");
const result = await db.query("SELECT * FROM users WHERE id = '1'") as Record<string, unknown>[];
expect(result[0].age).toBe(31);
});
it('SQL DELETE', async () => {
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
await db.query("DELETE FROM users WHERE id = '1'");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(0);
});
it('SQL DROP TABLE', async () => {
await db.query('DROP TABLE users');
const tables = await db.getTableNames();
expect(tables).not.toContain('users');
});
});
describe('事务', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
});
});
it('事务中执行多个操作', async () => {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
const table = db.table('users');
expect(await table.count()).toBe(2);
});
});
describe('表管理高级', () => {
beforeEach(async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
});
it('getTableNames 返回所有表', async () => {
const names = await db.getTableNames();
expect(names).toContain('users');
});
it('dropTable 后表不再存在', async () => {
await db.dropTable('users');
const names = await db.getTableNames();
expect(names).not.toContain('users');
});
it('Table.getSchema 返回表结构', async () => {
const table = db.table('users');
const schema = await table.getSchema();
expect(schema.name).toBe('users');
expect(schema.columns.id.primaryKey).toBe(true);
});
it('Table.clear 清空数据', async () => {
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice' });
await table.insert({ id: '2', name: 'Bob' });
await table.clear();
expect(await table.count()).toBe(0);
expect(await db.getTableNames()).toContain('users');
});
it('Table.drop 删除表', async () => {
const table = db.table('users');
await table.drop();
const names = await db.getTableNames();
expect(names).not.toContain('users');
});
});
});
// ===================================================================
// Disk 模式
// ===================================================================
describe('MetonaSqlark Disk 模式', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `test-disk-${++diskCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'indexeddb' });
await db.init();
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('创建 disk 模式数据库', () => {
expect(db.isReady()).toBe(true);
expect(db.mode).toBe('disk');
});
it('定义表并插入数据', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('users').insert({ id: '1', name: 'Alice' });
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(1);
});
});
// ===================================================================
// Hybrid 模式
// ===================================================================
describe('MetonaSqlark Hybrid 模式', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `test-hybrid-core-${++diskCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
await db.init();
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('创建 hybrid 模式数据库', () => {
expect(db.isReady()).toBe(true);
expect(db.mode).toBe('hybrid');
});
it('基本 CRUD', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
const table = db.table('users');
await table.insert({ id: '1', name: 'Alice' });
expect(await table.count()).toBe(1);
await table.update({ name: 'Alice2' }).where({ id: '1' }).execute();
const rows = await table.select().where({ id: '1' }).execute();
expect(rows[0].name).toBe('Alice2');
await table.delete().where({ id: '1' }).execute();
expect(await table.count()).toBe(0);
});
it('SQL 查询', async () => {
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
const result = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(result).toHaveLength(1);
});
});
+210
View File
@@ -0,0 +1,210 @@
/**
* Executor + Builder + Memory 边缘覆盖测试
*/
import { MetonaSqlark } from '../src/core';
import { MemoryEngine } from '../src/engine/memory';
import { SelectQueryBuilder } from '../src/query/builder';
import { createSchema } from '../src/table/schema';
describe('边缘覆盖', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-edge', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number' },
active: { type: 'boolean', default: true },
});
await db.table('users').insertMany([
{ id: '1', name: 'Alice', age: 30, active: true },
{ id: '2', name: 'Bob', age: 25, active: true },
{ id: '3', name: 'Charlie', age: 35, active: false },
{ id: '4', name: 'Diana', age: 30, active: true },
]);
});
afterEach(async () => { await db.close(); });
// ---- DISTINCT ----
describe('DISTINCT', () => {
it('SELECT DISTINCT 去重', async () => {
const result = await db.query('SELECT DISTINCT age FROM users ORDER BY age') as Record<string, unknown>[];
expect(result).toHaveLength(3); // 30,25,35
});
it('DISTINCT with WHERE', async () => {
const result = await db.query("SELECT DISTINCT age FROM users WHERE active = TRUE") as Record<string, unknown>[];
expect(result).toHaveLength(2); // 30,25
});
});
// ---- $and / $or / $not ----
describe('复杂 Where 条件', () => {
it('$and 条件 (MemoryEngine 直接)', async () => {
// $and 在 MemoryEngine 的 matchWhere 中解析
const engine = new MemoryEngine();
await engine.open('test', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, age: { type: 'number' }, active: { type: 'boolean' },
}));
await engine.insert('users', [
{ id: '1', age: 30, active: true },
{ id: '2', age: 25, active: true },
{ id: '3', age: 30, active: false },
]);
const rows = await engine.find('users', {
table: 'users',
where: { $and: [{ age: 30 }, { active: true }] } as any,
});
expect(rows).toHaveLength(1);
await engine.close();
});
it('$or 条件 (MemoryEngine 直接)', async () => {
const engine = new MemoryEngine();
await engine.open('test2', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, name: { type: 'string' },
}));
await engine.insert('users', [
{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, { id: '3', name: 'Charlie' },
]);
const rows = await engine.find('users', {
table: 'users',
where: { $or: [{ name: 'Alice' }, { name: 'Bob' }] } as any,
});
expect(rows).toHaveLength(2);
await engine.close();
});
it('$not 条件', async () => {
const rows = await db.table('users').select().where({
name: { $not: { $eq: 'Alice' } } as any,
}).execute();
expect(rows).toHaveLength(3);
});
it('$in + $nin', async () => {
const r1 = await db.table('users').select().where({ age: { $in: [25, 35] } }).execute();
expect(r1).toHaveLength(2);
const r2 = await db.table('users').select().where({ age: { $nin: [30] } }).execute();
expect(r2).toHaveLength(2);
});
it('$lt + $lte', async () => {
const r = await db.table('users').select().where({ age: { $lt: 30 } }).execute();
expect(r).toHaveLength(1); // Bob(25)
const r2 = await db.table('users').select().where({ age: { $lte: 30 } }).execute();
expect(r2).toHaveLength(3); // Alice+Diana(30)+Bob(25)
});
});
// ---- QueryBuilder 全覆盖 ----
describe('QueryBuilder 全覆盖', () => {
let engine: MemoryEngine;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
}));
await engine.insert('users', [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
});
afterEach(async () => { await engine.close(); });
it('SelectQueryBuilder.as 别名', () => {
const qb = new SelectQueryBuilder(engine, 'users').as('u');
expect(qb.toAST().alias).toBe('u');
});
it('rightJoin', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.rightJoin('orders', { 'users.id': { $col: 'orders.user_id' } }, 'o');
expect(qb.toAST().joins![0].type).toBe('RIGHT');
});
it('join 默认为 INNER', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.join('orders', { 'users.id': { $col: 'orders.user_id' } });
expect(qb.toAST().joins![0].type).toBe('INNER');
});
});
// ---- RIGHT JOIN SQL ----
describe('RIGHT JOIN', () => {
beforeEach(async () => {
await db.defineTable('departments', {
id: { type: 'number', primaryKey: true },
name: { type: 'string' },
});
await db.table('departments').insertMany([
{ id: 1, name: 'Eng' }, { id: 2, name: 'Sales' },
]);
await db.defineTable('emp', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
dept_id: { type: 'number' },
});
await db.table('emp').insert({ id: '1', name: 'Alice', dept_id: 1 });
});
it('RIGHT JOIN 保留右表无匹配行', async () => {
const result = await db.query(
'SELECT emp.name, departments.name FROM emp RIGHT JOIN departments ON emp.dept_id = departments.id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(2);
});
});
// ---- MemoryEngine 索引 ----
describe('MemoryEngine 索引查找', () => {
it('索引列 $eq 查询', async () => {
// age 列标记为 index
await db.defineTable('indexed_users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
score: { type: 'number', index: true },
});
await db.table('indexed_users').insertMany([
{ id: '1', name: 'A', score: 100 },
{ id: '2', name: 'B', score: 200 },
{ id: '3', name: 'C', score: 100 },
]);
// $eq 查询应使用索引
const rows = await db.table('indexed_users').select().where({ score: 100 }).execute();
expect(rows).toHaveLength(2);
});
it('唯一索引列查询', async () => {
await db.defineTable('unique_users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.table('unique_users').insertMany([
{ id: '1', email: 'a@t.com' },
{ id: '2', email: 'b@t.com' },
]);
const rows = await db.table('unique_users').select().where({ email: 'a@t.com' }).execute();
expect(rows).toHaveLength(1);
});
});
// ---- GROUP BY 边缘 ----
describe('GROUP BY 边缘', () => {
it('单个 GROUP BY 列', async () => {
const result = await db.query('SELECT active, COUNT(*) FROM users GROUP BY active') as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('AVG 空值处理 (GROUP BY)', async () => {
await db.defineTable('scores', { id: { type: 'string', primaryKey: true }, dept: { type: 'string' }, val: { type: 'number' } });
await db.table('scores').insertMany([{ id: '1', dept: 'A', val: 100 }, { id: '2', dept: 'A', val: 200 }]);
const result = await db.query('SELECT dept, AVG(val) FROM scores GROUP BY dept') as Record<string, unknown>[];
expect(result[0]['AVG(val)']).toBe(150);
});
});
});
+323
View File
@@ -0,0 +1,323 @@
/**
* IndexedDBEngine 完整测试(使用 fake-indexeddb
* 每次测试使用独立的数据库名避免版本冲突
*/
import 'fake-indexeddb/auto';
import { IndexedDBEngine } from '../../src/engine/indexeddb';
import { createSchema } from '../../src/table/schema';
let dbCounter = 0;
describe('IndexedDBEngine', () => {
let engine: IndexedDBEngine;
let dbName: string;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
});
beforeEach(async () => {
dbName = `test-idb-${++dbCounter}`;
engine = new IndexedDBEngine();
await engine.open(dbName, 1);
});
afterEach(async () => {
if (engine.isOpen()) {
await engine.close();
}
// 清理:删除 IndexedDB 数据库
try {
indexedDB.deleteDatabase(dbName);
} catch {}
});
// ---- 生命周期 ----
describe('生命周期', () => {
it('打开和关闭', async () => {
expect(engine.isOpen()).toBe(true);
await engine.close();
expect(engine.isOpen()).toBe(false);
});
it('未打开时操作抛出错误', async () => {
await engine.close();
await expect(engine.count('users')).rejects.toThrow('not opened');
});
it('可以重新打开', async () => {
await engine.close();
await engine.open(dbName, 2);
expect(engine.isOpen()).toBe(true);
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建和删除表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
it('表名列表', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.name).toBe('users');
});
it('获取不存在的表结构返回 null', async () => {
const schema = await engine.getTableSchema('nonexistent');
expect(schema).toBeNull();
});
});
// ---- 插入 ----
describe('插入', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入单行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
]);
expect(pks).toEqual(['1']);
expect(await engine.count('users')).toBe(1);
});
it('插入多行', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
expect(await engine.count('users')).toBe(2);
});
it('必填字段缺失抛出错误', async () => {
await expect(
engine.insert('users', [{ id: '1', email: 'a@t.com' }]),
).rejects.toThrow('required');
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@test.com' },
]);
});
it('查询所有', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE 条件', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 28 } },
});
expect(rows).toHaveLength(2);
});
it('ORDER BY + LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
limit: 2,
});
expect(rows.map(r => r.age)).toEqual([35, 30]);
});
it('列选择', async () => {
const rows = await engine.find('users', {
table: 'users',
columns: ['id'],
where: { id: '1' },
});
expect(Object.keys(rows[0])).toEqual(['id']);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('更新匹配行', async () => {
const count = await engine.update(
'users',
{ table: 'users', where: { id: '1' } },
{ age: 31 },
);
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('更新所有行', async () => {
const count = await engine.update(
'users',
{ table: 'users' },
{ age: 100 },
);
expect(count).toBe(2);
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('条件删除', async () => {
const count = await engine.delete('users', {
table: 'users',
where: { id: '1' },
});
expect(count).toBe(1);
expect(await engine.count('users')).toBe(1);
});
it('清空表', async () => {
await engine.clear('users');
expect(await engine.count('users')).toBe(0);
// 表结构仍存在
expect(await engine.hasTable('users')).toBe(true);
});
it('删除不存在的表抛出错误', async () => {
await expect(
engine.delete('nonexistent', { table: 'nonexistent' }),
).rejects.toThrow();
});
});
// ---- Count ----
describe('Count', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
]);
});
it('count 全部', async () => {
expect(await engine.count('users')).toBe(2);
});
it('count with where', async () => {
expect(await engine.count('users', {
table: 'users',
where: { age: { $gt: 28 } },
})).toBe(1);
});
});
// ---- 复杂 Where 条件 ----
describe('复杂 Where', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'c@t.com' },
]);
});
it('$like 模糊匹配', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'A%' } },
});
expect(rows).toHaveLength(1);
});
it('$in 列表', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { id: { $in: ['1', '3'] } },
});
expect(rows).toHaveLength(2);
});
it('$ne 不等于', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $ne: 25 } },
});
expect(rows).toHaveLength(2);
});
it('$gte $lte 范围', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gte: 25, $lte: 30 } },
});
expect(rows).toHaveLength(2);
});
});
// ---- 并发/多次操作 ----
describe('多次操作', () => {
it('连续创建删除表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('插入后查询再更新再查询', async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
]);
let rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(30);
await engine.update('users', { table: 'users' }, { age: 31 });
rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(31);
});
});
});
+272
View File
@@ -0,0 +1,272 @@
/**
* MemoryEngine 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { createSchema } from '../../src/table/schema';
describe('MemoryEngine', () => {
let engine: MemoryEngine;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
email: { type: 'string', unique: true },
});
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-db', 1);
});
afterEach(async () => {
await engine.close();
});
// ---- 生命周期 ----
describe('open/close', () => {
it('打开后 isOpen 返回 true', () => {
expect(engine.isOpen()).toBe(true);
});
it('关闭后 isOpen 返回 false', async () => {
await engine.close();
expect(engine.isOpen()).toBe(false);
});
});
// ---- 表管理 ----
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('重复创建表抛出错误', async () => {
await engine.createTable(userSchema);
await expect(engine.createTable(userSchema)).rejects.toThrow('already exists');
});
it('获取所有表名', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
});
// ---- CRUD ----
describe('插入', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入单行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
]);
expect(pks).toEqual(['1']);
});
it('插入多行', async () => {
const pks = await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
expect(pks).toEqual(['1', '2']);
});
it('重复主键抛出错误', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
await expect(
engine.insert('users', [{ id: '1', name: 'Duplicate', email: 'd@t.com' }]),
).rejects.toThrow('Duplicate');
});
it('唯一约束冲突', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'same@test.com' }]);
await expect(
engine.insert('users', [{ id: '2', name: 'Bob', email: 'same@test.com' }]),
).rejects.toThrow('Unique constraint');
});
it('必填字段缺失', async () => {
await expect(
engine.insert('users', [{ id: '1' }]),
).rejects.toThrow('required');
});
it('默认值填充', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', email: 'a@t.com' }]);
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(0);
});
it('类型错误', async () => {
await expect(
engine.insert('users', [{ id: '1', name: 'Alice', age: 'not-a-number', email: 'a@t.com' }]),
).rejects.toThrow('expects number');
});
});
// ---- 查询 ----
describe('查询', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@test.com' },
]);
});
it('查询所有行', async () => {
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(3);
});
it('WHERE $gt', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $gt: 28 } },
});
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
});
it('WHERE $eq', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: 'Alice' },
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
});
it('WHERE $in', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { age: { $in: [25, 35] } },
});
expect(rows).toHaveLength(2);
});
it('WHERE $like', async () => {
const rows = await engine.find('users', {
table: 'users',
where: { name: { $like: 'A%' } },
});
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
});
it('ORDER BY asc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'asc' }],
});
expect(rows.map((r) => r.age)).toEqual([25, 30, 35]);
});
it('ORDER BY desc', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
});
expect(rows.map((r) => r.age)).toEqual([35, 30, 25]);
});
it('LIMIT', async () => {
const rows = await engine.find('users', {
table: 'users',
limit: 2,
});
expect(rows).toHaveLength(2);
});
it('OFFSET', async () => {
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'id', direction: 'asc' }],
offset: 1,
limit: 1,
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('2');
});
it('列选择', async () => {
const rows = await engine.find('users', {
table: 'users',
columns: ['id', 'name'],
where: { id: '1' },
});
expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']);
});
});
// ---- 更新 ----
describe('更新', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('更新单行', async () => {
const count = await engine.update('users',
{ table: 'users', where: { id: '1' } },
{ age: 31 },
);
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].age).toBe(31);
});
it('更新所有行', async () => {
const count = await engine.update('users',
{ table: 'users' },
{ age: 100 },
);
expect(count).toBe(2);
});
});
// ---- 删除 ----
describe('删除', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30, email: 'alice@test.com' },
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.com' },
]);
});
it('条件删除', async () => {
const count = await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(count).toBe(1);
const rows = await engine.find('users', { table: 'users' });
expect(rows).toHaveLength(1);
});
it('清空表', async () => {
await engine.clear('users');
const count = await engine.count('users');
expect(count).toBe(0);
});
});
});
+118
View File
@@ -0,0 +1,118 @@
/**
* GROUP BY + 聚合函数测试 (v0.1.2)
*/
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
describe('GROUP BY (v0.1.2)', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-groupby', mode: 'memory' });
await db.init();
await db.defineTable('employees', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
dept: { type: 'string' },
salary: { type: 'number' },
});
await db.table('employees').insertMany([
{ id: '1', name: 'Alice', dept: 'Eng', salary: 1000 },
{ id: '2', name: 'Bob', dept: 'Eng', salary: 1200 },
{ id: '3', name: 'Charlie', dept: 'Sales', salary: 900 },
{ id: '4', name: 'Diana', dept: 'Sales', salary: 1100 },
{ id: '5', name: 'Eve', dept: 'HR', salary: 800 },
]);
});
afterEach(async () => {
await db.close();
});
// ===================================================================
// SQL GROUP BY
// ===================================================================
describe('SQL GROUP BY', () => {
it('GROUP BY + COUNT', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['COUNT(*)']).toBe(2);
});
it('GROUP BY + SUM', async () => {
const result = await db.query(
'SELECT dept, SUM(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['SUM(salary)']).toBe(2200);
});
it('GROUP BY + AVG', async () => {
const result = await db.query(
'SELECT dept, AVG(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['AVG(salary)']).toBe(1100);
});
it('GROUP BY + MIN/MAX', async () => {
const result = await db.query(
'SELECT dept, MIN(salary), MAX(salary) FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!['MIN(salary)']).toBe(1000);
expect(eng!['MAX(salary)']).toBe(1200);
});
it('GROUP BY + HAVING', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1',
) as Record<string, unknown>[];
expect(result).toHaveLength(2); // Eng(2) + Sales(2)
});
it('GROUP BY + WHERE + HAVING', async () => {
const result = await db.query(
"SELECT dept, SUM(salary) FROM employees WHERE dept != 'HR' GROUP BY dept HAVING SUM(salary) > 2000",
) as Record<string, unknown>[];
expect(result).toHaveLength(1); // Only Eng
});
it('聚合函数别名', async () => {
const result = await db.query(
'SELECT dept, COUNT(*) AS cnt, SUM(salary) AS total FROM employees GROUP BY dept',
) as Record<string, unknown>[];
const eng = result.find((r: any) => r.dept === 'Eng');
expect(eng!.cnt).toBe(2);
expect(eng!.total).toBe(2200);
});
});
// ===================================================================
// Parser 测试
// ===================================================================
describe('Parser', () => {
it('解析 COUNT(*)', () => {
const ast = parse('SELECT COUNT(*) FROM employees');
expect(ast.columns[0]).toBe('COUNT(*)');
});
it('解析 GROUP BY', () => {
const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept');
expect(ast.groupBy).toEqual(['dept']);
});
it('解析 HAVING', () => {
const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1');
expect(ast.having).toBeDefined();
});
});
});
+140
View File
@@ -0,0 +1,140 @@
/**
* HybridEngine 测试 — write-through 策略验证
* 注:re-open 持久化测试需要真实 IndexedDB 版本管理,fake-indexeddb 不支持。
* 这里验证 write-through 即时效正确性。
*/
import { HybridEngine } from '../../src/hybrid/index';
import { createSchema } from '../../src/table/schema';
import 'fake-indexeddb/auto';
let hybridCounter = 0;
describe('HybridEngine', () => {
let engine: HybridEngine;
let dbName: string;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
});
beforeEach(async () => {
dbName = `test-hybrid-${++hybridCounter}`;
engine = new HybridEngine('indexeddb');
await engine.open(dbName, 1);
});
afterEach(async () => {
if (engine.isOpen()) {
await engine.close();
}
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('引擎名称', () => {
expect(engine.name).toBe('hybrid');
});
it('磁盘引擎类型', () => {
expect(engine.getDiskEngineType()).toBe('indexeddb');
});
describe('表管理', () => {
it('创建表', async () => {
await engine.createTable(userSchema);
expect(await engine.hasTable('users')).toBe(true);
});
it('删除表', async () => {
await engine.createTable(userSchema);
await engine.dropTable('users');
expect(await engine.hasTable('users')).toBe(false);
});
it('表名列表', async () => {
await engine.createTable(userSchema);
const names = await engine.getTableNames();
expect(names).toContain('users');
});
it('获取表结构', async () => {
await engine.createTable(userSchema);
const schema = await engine.getTableSchema('users');
expect(schema).not.toBeNull();
expect(schema!.name).toBe('users');
});
});
describe('Write-Through 即时性', () => {
beforeEach(async () => {
await engine.createTable(userSchema);
});
it('插入后内存立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
const data = await engine.find('users', { table: 'users' });
expect(data).toHaveLength(2);
expect(await engine.count('users')).toBe(2);
});
it('更新后立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
]);
await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 31 });
const rows = await engine.find('users', { table: 'users' });
expect(rows[0].age).toBe(31);
});
it('删除后立即可读', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
]);
await engine.delete('users', { table: 'users', where: { id: '1' } });
expect(await engine.count('users')).toBe(1);
const rows = await engine.find('users', { table: 'users' });
expect(rows.map(r => r.id)).toEqual(['2']);
});
it('查询带排序和分页', async () => {
await engine.insert('users', [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Charlie', age: 35 },
]);
const rows = await engine.find('users', {
table: 'users',
orderBy: [{ column: 'age', direction: 'desc' }],
limit: 2,
});
expect(rows.map(r => r.age)).toEqual([35, 30]);
});
it('clear 清空表', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
await engine.clear('users');
expect(await engine.count('users')).toBe(0);
});
});
describe('生命周期', () => {
it('isOpen 状态', async () => {
expect(engine.isOpen()).toBe(true);
await engine.close();
expect(engine.isOpen()).toBe(false);
});
});
describe('内存引擎访问', () => {
it('getMemoryEngine 返回内存引擎', () => {
const mem = engine.getMemoryEngine();
expect(mem.name).toBe('memory');
});
});
});
+209
View File
@@ -0,0 +1,209 @@
/**
* JOIN 功能测试 (v0.1.1)
*/
import { MetonaSqlark } from '../src/core';
describe('JOIN (v0.1.1)', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'test-join', mode: 'memory' });
await db.init();
// 创建测试表
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
dept_id: { type: 'number' },
});
await db.defineTable('departments', {
id: { type: 'number', primaryKey: true },
name: { type: 'string', required: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
// 插入测试数据
await db.table('users').insertMany([
{ id: '1', name: 'Alice', dept_id: 1 },
{ id: '2', name: 'Bob', dept_id: 1 },
{ id: '3', name: 'Charlie', dept_id: 2 },
]);
await db.table('departments').insertMany([
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Sales' },
{ id: 3, name: 'HR' }, // 无用户
]);
await db.table('orders').insertMany([
{ id: 'o1', user_id: '1', amount: 100 },
{ id: 'o2', user_id: '1', amount: 200 },
{ id: 'o3', user_id: '2', amount: 150 },
]);
});
afterEach(async () => {
await db.close();
});
// ===================================================================
// SQL JOIN
// ===================================================================
describe('SQL JOIN', () => {
it('INNER JOIN', async () => {
const result = await db.query(
'SELECT users.name, departments.name FROM users INNER JOIN departments ON users.dept_id = departments.id',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
});
it('LEFT JOIN', async () => {
const result = await db.query(
'SELECT departments.name FROM departments LEFT JOIN users ON departments.id = users.dept_id',
) as Record<string, unknown>[];
// HR 部门没有用户,LEFT JOIN 应保留
expect(result.length).toBeGreaterThanOrEqual(4);
});
it('CROSS JOIN', async () => {
const result = await db.query(
'SELECT * FROM users CROSS JOIN departments',
) as Record<string, unknown>[];
// 3 users × 3 departments = 9
expect(result).toHaveLength(9);
});
it('JOIN with alias', async () => {
const result = await db.query(
'SELECT u.name, d.name FROM users u INNER JOIN departments d ON u.dept_id = d.id',
) as Record<string, unknown>[];
expect(result).toHaveLength(3);
});
it('JOIN with AS alias', async () => {
const result = await db.query(
'SELECT u.name FROM users AS u INNER JOIN orders AS o ON u.id = o.user_id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(3);
});
it('JOIN with WHERE', async () => {
const result = await db.query(
'SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE o.amount > 120',
) as Record<string, unknown>[];
expect(result).toHaveLength(2); // o2(200) + o3(150)
});
it('Multiple JOIN', async () => {
const result = await db.query(
'SELECT u.name, d.name, o.amount FROM users u INNER JOIN departments d ON u.dept_id = d.id INNER JOIN orders o ON u.id = o.user_id',
) as Record<string, unknown>[];
expect(result.length).toBeGreaterThanOrEqual(3);
});
it('LEFT JOIN with no match returns null', async () => {
const result = await db.query(
'SELECT d.name, u.name FROM departments d LEFT JOIN users u ON d.id = u.dept_id WHERE d.id = 3',
) as Record<string, unknown>[];
expect(result).toHaveLength(1);
// HR 部门无用户,u.name 应为 null
// Note: column projection may vary
});
});
// ===================================================================
// QueryBuilder JOIN
// ===================================================================
describe('QueryBuilder JOIN', () => {
it('innerJoin via QueryBuilder', async () => {
const results = await db.table('users')
.select(['users.name', 'departments.name'])
.innerJoin('departments', { 'users.dept_id': { $col: 'departments.id' } })
.execute();
expect(results).toHaveLength(3);
});
it('leftJoin via QueryBuilder', async () => {
const results = await db.table('departments')
.select()
.leftJoin('users', { 'departments.id': { $col: 'users.dept_id' } })
.execute();
expect(results.length).toBeGreaterThanOrEqual(4);
});
it('crossJoin via QueryBuilder', async () => {
const results = await db.table('users')
.select()
.crossJoin('departments')
.execute();
expect(results).toHaveLength(9);
});
it('join with where + orderBy + limit', async () => {
const results = await db.table('users')
.select()
.innerJoin('orders', { 'users.id': { $col: 'orders.user_id' } })
.where({ 'orders.amount': { $gt: 100 } })
.orderBy('orders.amount', 'desc')
.limit(2)
.execute();
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results.length).toBeLessThanOrEqual(2);
// 按 amount desc 排序
if (results.length === 2) {
expect((results[0] as any)['orders.amount']).toBeGreaterThanOrEqual(
(results[1] as any)['orders.amount'],
);
}
});
});
// ===================================================================
// Parser JOIN 语法
// ===================================================================
describe('Parser JOIN 语法', () => {
it('INNER JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users INNER JOIN orders ON users.id = orders.user_id');
expect(ast.joins).toHaveLength(1);
expect(ast.joins[0].type).toBe('INNER');
expect(ast.joins[0].table).toBe('orders');
});
it('LEFT OUTER JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users LEFT OUTER JOIN orders ON users.id = orders.user_id');
expect(ast.joins[0].type).toBe('LEFT');
});
it('RIGHT JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users RIGHT JOIN orders ON users.id = orders.user_id');
expect(ast.joins[0].type).toBe('RIGHT');
});
it('CROSS JOIN 解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT * FROM users CROSS JOIN departments');
expect(ast.joins[0].type).toBe('CROSS');
});
it('表别名解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT u.name FROM users u');
expect(ast.alias).toBe('u');
});
it('AS 别名解析', () => {
const { parse } = require('../src/sql/parser');
const ast = parse('SELECT u.name FROM users AS u');
expect(ast.alias).toBe('u');
});
});
});
+182
View File
@@ -0,0 +1,182 @@
/**
* PluginManager 完整测试
*/
import { PluginManager } from '../../src/plugin/index';
import type { MetonaPlugin } from '../../src/constants';
describe('PluginManager', () => {
let pm: PluginManager;
beforeEach(() => {
pm = new PluginManager();
});
// ---- 注册/卸载 ----
describe('注册和卸载', () => {
it('注册插件', () => {
const plugin: MetonaPlugin = {
name: 'test-plugin',
version: '1.0.0',
install: jest.fn(),
destroy: jest.fn(),
};
pm.register(plugin);
expect(pm.getPlugins()).toHaveLength(1);
expect(plugin.install).toHaveBeenCalled();
});
it('按优先级排序', () => {
const p1: MetonaPlugin = { name: 'low', version: '1.0', priority: 1, install: jest.fn(), destroy: jest.fn() };
const p2: MetonaPlugin = { name: 'high', version: '1.0', priority: 10, install: jest.fn(), destroy: jest.fn() };
const p3: MetonaPlugin = { name: 'mid', version: '1.0', priority: 5, install: jest.fn(), destroy: jest.fn() };
pm.register(p1);
pm.register(p2);
pm.register(p3);
const plugins = pm.getPlugins();
expect(plugins[0].name).toBe('high');
expect(plugins[1].name).toBe('mid');
expect(plugins[2].name).toBe('low');
});
it('卸载插件调用 destroy', () => {
const destroyFn = jest.fn();
const plugin: MetonaPlugin = {
name: 'test',
version: '1.0',
install: jest.fn(),
destroy: destroyFn,
};
pm.register(plugin);
pm.unregister('test');
expect(destroyFn).toHaveBeenCalled();
expect(pm.getPlugins()).toHaveLength(0);
});
it('卸载不存在的插件不报错', () => {
expect(() => pm.unregister('nonexistent')).not.toThrow();
});
it('getPlugins 返回副本', () => {
const plugin: MetonaPlugin = {
name: 'test', version: '1.0', install: jest.fn(), destroy: jest.fn(),
};
pm.register(plugin);
const plugins = pm.getPlugins();
plugins.push({ name: 'external', version: '1.0', install: jest.fn(), destroy: jest.fn() });
expect(pm.getPlugins()).toHaveLength(1);
});
});
// ---- 钩子 ----
describe('钩子系统', () => {
it('注册和触发钩子', async () => {
const callback = jest.fn();
pm.on('beforeInsert', callback);
await pm.trigger('beforeInsert', { id: '1' });
expect(callback).toHaveBeenCalledWith({ id: '1' });
});
it('多个回调', async () => {
const cb1 = jest.fn();
const cb2 = jest.fn();
pm.on('afterQuery', cb1);
pm.on('afterQuery', cb2);
await pm.trigger('afterQuery', 'SELECT *', []);
expect(cb1).toHaveBeenCalled();
expect(cb2).toHaveBeenCalled();
});
it('卸载钩子', async () => {
const cb = jest.fn();
pm.on('beforeUpdate', cb);
pm.off('beforeUpdate', cb);
await pm.trigger('beforeUpdate');
expect(cb).not.toHaveBeenCalled();
});
it('触发未注册的钩子不报错', async () => {
await expect(pm.trigger('beforeInsert')).resolves.toBeUndefined();
});
it('卸载不存在的回调不报错', () => {
expect(() => pm.off('beforeInsert', jest.fn())).not.toThrow();
});
it('异步回调', async () => {
const cb = jest.fn().mockResolvedValue(undefined);
pm.on('beforeQuery', cb);
await pm.trigger('beforeQuery');
expect(cb).toHaveBeenCalled();
});
});
// ---- 生命周期钩子覆盖 ----
describe('全部钩子类型', () => {
const hooks = [
'beforeCreateTable', 'afterCreateTable',
'beforeDropTable', 'afterDropTable',
'beforeInsert', 'afterInsert',
'beforeUpdate', 'afterUpdate',
'beforeDelete', 'afterDelete',
'beforeQuery', 'afterQuery',
'beforeTransaction', 'afterTransaction',
] as const;
hooks.forEach((hook) => {
it(`钩子 ${hook} 可注册和触发`, async () => {
const cb = jest.fn();
pm.on(hook, cb);
await pm.trigger(hook);
expect(cb).toHaveBeenCalledTimes(1);
});
});
});
// ---- destroy ----
describe('销毁', () => {
it('destroy 调用所有插件的 destroy', () => {
const d1 = jest.fn();
const d2 = jest.fn();
pm.register({ name: 'p1', version: '1.0', install: jest.fn(), destroy: d1 });
pm.register({ name: 'p2', version: '1.0', install: jest.fn(), destroy: d2 });
pm.destroy();
expect(d1).toHaveBeenCalled();
expect(d2).toHaveBeenCalled();
expect(pm.getPlugins()).toHaveLength(0);
});
it('destroy 清空钩子', async () => {
const cb = jest.fn();
pm.on('beforeInsert', cb);
pm.destroy();
await pm.trigger('beforeInsert');
expect(cb).not.toHaveBeenCalled();
});
it('插件 destroy 出错不影响销毁流程', () => {
const badPlugin: MetonaPlugin = {
name: 'bad',
version: '1.0',
install: jest.fn(),
destroy: () => { throw new Error('destroy failed'); },
};
const goodPlugin: MetonaPlugin = {
name: 'good',
version: '1.0',
install: jest.fn(),
destroy: jest.fn(),
};
pm.register(badPlugin);
pm.register(goodPlugin);
expect(() => pm.destroy()).not.toThrow();
expect(goodPlugin.destroy).toHaveBeenCalled();
});
});
});
+202
View File
@@ -0,0 +1,202 @@
/**
* QueryBuilder 和 Compiler/Executor 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { createSchema } from '../../src/table/schema';
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../../src/query/builder';
import { compileStatement } from '../../src/query/compiler';
import { QueryExecutor } from '../../src/query/executor';
import { parse } from '../../src/sql/parser';
describe('QueryBuilder', () => {
let engine: MemoryEngine;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-qb', 1);
});
afterEach(async () => {
await engine.close();
});
// ---- SelectQueryBuilder ----
describe('SelectQueryBuilder', () => {
it('基础查询', () => {
const qb = new SelectQueryBuilder(engine, 'users');
expect(qb.toAST()).toMatchObject({
type: 'SELECT',
columns: ['*'],
from: 'users',
where: {},
});
});
it('链式调用', () => {
const qb = new SelectQueryBuilder(engine, 'users', ['id', 'name']);
qb.where({ age: { $gt: 18 } }).orderBy('name', 'asc').limit(10).offset(5);
const ast = qb.toAST();
expect(ast.columns).toEqual(['id', 'name']);
expect(ast.where).toEqual({ age: { $gt: 18 } });
expect(ast.orderBy).toEqual([{ column: 'name', direction: 'asc' }]);
expect(ast.limit).toBe(10);
expect(ast.offset).toBe(5);
});
it('多次 where 合并条件', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.where({ age: { $gt: 18 } }).where({ active: true });
expect(qb.toAST().where).toEqual({ age: { $gt: 18 }, active: true });
});
it('多次 orderBy 追加排序', () => {
const qb = new SelectQueryBuilder(engine, 'users');
qb.orderBy('age', 'desc').orderBy('name', 'asc');
expect(qb.toAST().orderBy).toHaveLength(2);
});
});
// ---- UpdateQueryBuilder ----
describe('UpdateQueryBuilder', () => {
it('基础更新', () => {
const qb = new UpdateQueryBuilder(engine, 'users', { age: 31 });
qb.where({ id: '1' });
const ast = qb.toAST();
expect(ast).toMatchObject({
type: 'UPDATE',
table: 'users',
sets: { age: 31 },
where: { id: '1' },
});
});
it('不带 where', () => {
const qb = new UpdateQueryBuilder(engine, 'users', { active: false });
expect(qb.toAST().where).toEqual({});
});
});
// ---- DeleteQueryBuilder ----
describe('DeleteQueryBuilder', () => {
it('基础删除', () => {
const qb = new DeleteQueryBuilder(engine, 'users');
qb.where({ id: '1' });
const ast = qb.toAST();
expect(ast).toMatchObject({
type: 'DELETE',
from: 'users',
where: { id: '1' },
});
});
it('不带 where', () => {
const qb = new DeleteQueryBuilder(engine, 'users');
expect(qb.toAST().where).toEqual({});
});
});
});
// ===================================================================
describe('Compiler', () => {
it('编译 SELECT', () => {
const ast = parse('SELECT id, name FROM users WHERE age > 18 ORDER BY age DESC LIMIT 10');
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.columns).toEqual(['id', 'name']);
expect(plan.where).toBeDefined();
expect(plan.orderBy).toEqual([{ column: 'age', direction: 'desc' }]);
expect(plan.limit).toBe(10);
});
it('编译 DELETE', () => {
const ast = parse("DELETE FROM users WHERE id = '1'");
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.where).toBeDefined();
});
it('编译 UPDATE', () => {
const ast = parse("UPDATE users SET age = 31 WHERE id = '1'");
const plan = compileStatement(ast);
expect(plan.table).toBe('users');
expect(plan.where).toBeDefined();
});
it('非查询语句抛出错误', () => {
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY)');
expect(() => compileStatement(ast)).toThrow('Cannot compile');
});
});
// ===================================================================
describe('QueryExecutor', () => {
let engine: MemoryEngine;
let executor: QueryExecutor;
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-exec', 1);
executor = new QueryExecutor(engine);
});
afterEach(async () => {
await engine.close();
});
it('执行 CREATE TABLE', async () => {
const ast = parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)');
await executor.execute(ast);
expect(await engine.hasTable('users')).toBe(true);
});
it('执行 INSERT + SELECT', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
await executor.execute(parse("INSERT INTO users (id, name, age) VALUES ('1', 'Alice', 30)"));
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob', 25)"));
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
expect(result).toHaveLength(2);
});
it('执行 UPDATE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING, age NUMBER)'));
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice', 30)"));
await executor.execute(parse("UPDATE users SET age = 31 WHERE id = '1'"));
const result = await executor.execute(parse("SELECT * FROM users WHERE id = '1'")) as Record<string, unknown>[];
expect(result[0].age).toBe(31);
});
it('执行 DELETE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY, name STRING)'));
await executor.execute(parse("INSERT INTO users VALUES ('1', 'Alice')"));
await executor.execute(parse("INSERT INTO users VALUES ('2', 'Bob')"));
await executor.execute(parse("DELETE FROM users WHERE id = '1'"));
const result = await executor.execute(parse('SELECT * FROM users')) as Record<string, unknown>[];
expect(result).toHaveLength(1);
});
it('执行 DROP TABLE', async () => {
await executor.execute(parse('CREATE TABLE users (id STRING PRIMARY KEY)'));
await executor.execute(parse('DROP TABLE users'));
expect(await engine.hasTable('users')).toBe(false);
});
it('操作不存在的表抛出错误', async () => {
await expect(
executor.execute(parse('SELECT * FROM nonexistent')),
).rejects.toThrow('does not exist');
});
it('getEngine', () => {
expect(executor.getEngine()).toBe(engine);
});
});
+96
View File
@@ -0,0 +1,96 @@
/**
* Lexer 边缘场景测试
*/
import { Lexer, tokenize } from '../../src/sql/lexer';
import { TokenType } from '../../src/sql/tokens';
describe('Lexer 边缘场景', () => {
it('空字符串返回 EOF', () => {
const tokens = tokenize('');
expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe(TokenType.EOF);
});
it('只包含空格', () => {
const tokens = tokenize(' \t\n ');
expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe(TokenType.EOF);
});
it('非法字符返回 ILLEGAL', () => {
const tokens = tokenize('@');
expect(tokens[0].type).toBe(TokenType.ILLEGAL);
});
it('完整的 CREATE TABLE 语句', () => {
const tokens = tokenize('CREATE TABLE users (id STRING PRIMARY KEY, name STRING NOT NULL, age NUMBER DEFAULT 0)');
const types = tokens.map(t => t.type);
expect(types).toEqual([
TokenType.CREATE, TokenType.TABLE, TokenType.IDENTIFIER, TokenType.LPAREN,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.PRIMARY, TokenType.KEY,
TokenType.COMMA,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.NOT, TokenType.NULL,
TokenType.COMMA,
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.DEFAULT, TokenType.NUMBER,
TokenType.RPAREN, TokenType.EOF,
]);
});
it('带转义单引号的字符串', () => {
// SQL 中两个单引号 '' 表示转义的单引号
const tokens = tokenize("SELECT 'hello world'");
expect(tokens[1].type).toBe(TokenType.STRING);
expect(tokens[1].value).toBe('hello world');
});
it('双引号字符串', () => {
const tokens = tokenize('SELECT "hello world"');
expect(tokens[1].type).toBe(TokenType.STRING);
expect(tokens[1].value).toBe('hello world');
});
it('所有关键字', () => {
const sql = 'SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE DROP ORDER BY ASC DESC LIMIT OFFSET AND OR NOT LIKE IN PRIMARY KEY UNIQUE DEFAULT NULL TRUE FALSE';
const tokens = tokenize(sql);
const nonEof = tokens.filter(t => t.type !== TokenType.EOF);
expect(nonEof).toHaveLength(30);
// 验证都是关键字,不是 IDENTIFIER
for (const t of nonEof) {
expect(t.type).not.toBe(TokenType.IDENTIFIER);
}
});
it('下划线标识符', () => {
const tokens = tokenize('SELECT my_col_1 FROM user_table');
expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
expect(tokens[1].value).toBe('my_col_1');
expect(tokens[3].type).toBe(TokenType.IDENTIFIER);
expect(tokens[3].value).toBe('user_table');
});
it('运算符分隔符', () => {
const tokens = tokenize('= != > >= < <= <> ( ) , ; *');
const types = tokens.map(t => t.type);
expect(types).toEqual([
TokenType.EQ, TokenType.NEQ, TokenType.GT, TokenType.GTE,
TokenType.LT, TokenType.LTE, TokenType.NEQ,
TokenType.LPAREN, TokenType.RPAREN, TokenType.COMMA,
TokenType.SEMICOLON, TokenType.STAR, TokenType.EOF,
]);
});
it('数字: 整数和浮点数和负数', () => {
const tokens = tokenize('42 3.14 -7 -2.5');
expect(tokens[0].value).toBe('42');
expect(tokens[1].value).toBe('3.14');
expect(tokens[2].value).toBe('-7');
expect(tokens[3].value).toBe('-2.5');
});
it('Token position 正确', () => {
const lexer = new Lexer('SELECT * FROM users');
const tok = lexer.nextToken();
expect(tok.position).toBe(0); // SELECT 从位置 0 开始
});
});
+153
View File
@@ -0,0 +1,153 @@
/**
* Parser 边缘场景测试
*/
import { parse } from '../../src/sql/parser';
describe('Parser 边缘场景', () => {
// ---- SELECT 扩展 ----
describe('SELECT 扩展', () => {
it('WHERE 多条件 AND', () => {
const ast = parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'");
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
});
it('WHERE IN 列表', () => {
const ast = parse('SELECT * FROM users WHERE id IN (1, 2, 3)');
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
const cond = (ast.where as any).id;
expect(cond.$in).toEqual([1, 2, 3]);
});
it('WHERE IS NULL', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
expect(ast.type).toBe('SELECT');
expect(ast.where).toBeDefined();
});
it('WHERE IS NOT NULL', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NOT NULL');
expect(ast.type).toBe('SELECT');
});
it('WHERE NOT', () => {
const ast = parse("SELECT * FROM users WHERE NOT age = 18");
expect(ast.type).toBe('SELECT');
});
it('WHERE 括号分组', () => {
const ast = parse("SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE");
expect(ast.type).toBe('SELECT');
});
it('ORDER BY 多列', () => {
const ast = parse('SELECT * FROM users ORDER BY age DESC, name ASC');
expect(ast.type).toBe('SELECT');
expect(ast.orderBy).toHaveLength(2);
expect(ast.orderBy![0]).toEqual({ column: 'age', direction: 'desc' });
expect(ast.orderBy![1]).toEqual({ column: 'name', direction: 'asc' });
});
it('只有 LIMIT 没有 OFFSET', () => {
const ast = parse('SELECT * FROM users LIMIT 5');
expect(ast.limit).toBe(5);
expect(ast.offset).toBeUndefined();
});
it('只有 OFFSET 没有 LIMIT', () => {
const ast = parse('SELECT * FROM users OFFSET 10');
expect(ast).toMatchObject({ offset: 10 });
});
it('不带 WHERE 的 SELECT', () => {
const ast = parse('SELECT id, name FROM users ORDER BY id LIMIT 5');
expect(ast.where).toEqual({});
});
});
// ---- INSERT 扩展 ----
describe('INSERT 扩展', () => {
it('不带列名的 INSERT', () => {
const ast = parse("INSERT INTO users VALUES ('1', 'Alice', 30)");
expect(ast.columns).toBeUndefined();
expect(ast.values).toEqual([['1', 'Alice', 30]]);
});
it('INSERT 布尔值和 NULL', () => {
const ast = parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)');
expect(ast.values[0]).toEqual([true, false, null]);
});
it('INSERT 负数和浮点数', () => {
const ast = parse('INSERT INTO scores VALUES (-1, 3.14)');
expect(ast.values[0]).toEqual([-1, 3.14]);
});
});
// ---- UPDATE 扩展 ----
describe('UPDATE 扩展', () => {
it('UPDATE 多列', () => {
const ast = parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'");
expect(ast.type).toBe('UPDATE');
expect(ast.sets).toEqual({ name: 'Bob', age: 26 });
});
it('UPDATE 不带 WHERE', () => {
const ast = parse('UPDATE users SET active = FALSE');
expect(ast.where).toEqual({});
});
});
// ---- DELETE 扩展 ----
describe('DELETE 扩展', () => {
it('DELETE 不带 WHERE', () => {
const ast = parse('DELETE FROM users');
expect(ast.where).toEqual({});
});
it('DELETE 带复杂 WHERE', () => {
const ast = parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'");
expect(ast.type).toBe('DELETE');
});
});
// ---- DDL 扩展 ----
describe('DDL 扩展', () => {
it('CREATE TABLE 完整修饰符', () => {
const ast = parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)");
expect(ast.type).toBe('CREATE_TABLE');
expect(ast.columns).toHaveLength(4);
expect(ast.columns[0]).toMatchObject({ name: 'id', primaryKey: true });
expect(ast.columns[1]).toMatchObject({ name: 'name', required: true, unique: true });
expect(ast.columns[2]).toMatchObject({ name: 'price', default: 0 });
expect(ast.columns[3]).toMatchObject({ name: 'active', default: true });
});
it('不支持 IF EXISTS 语法(表名被解析为 IF', () => {
// 解析器把 IF 当作表名,EXISTS 作为后续 token 导致错误
const ast = parse('DROP TABLE users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' });
});
});
// ---- 值类型 ----
describe('常量值解析', () => {
it('布尔值 TRUE/FALSE', () => {
const ast = parse("SELECT * FROM users WHERE active = TRUE");
expect(ast.where).toBeDefined();
});
it('NULL 值', () => {
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
expect(ast.where).toBeDefined();
});
});
});
+165
View File
@@ -0,0 +1,165 @@
/**
* Schema 边缘场景测试
*/
import { createSchema, validateRow, getPrimaryKey, checkFieldType, astColumnToColumnDef } from '../../src/table/schema';
describe('Schema 边缘场景', () => {
// ---- createSchema ----
describe('createSchema', () => {
it('空列表抛出错误', () => {
expect(() => createSchema('empty', {})).toThrow('at least one column');
});
it('多主键定义(取第一个标记的)', () => {
const schema = createSchema('multi', {
id1: { type: 'string', primaryKey: true },
id2: { type: 'string' },
});
expect(getPrimaryKey(schema)).toBe('id1');
});
it('大表定义', () => {
const cols: Record<string, any> = {};
for (let i = 0; i < 50; i++) {
cols[`col${i}`] = { type: 'string' };
}
cols.id = { type: 'string', primaryKey: true };
const schema = createSchema('big', cols);
expect(Object.keys(schema.columns)).toHaveLength(51);
});
});
// ---- getPrimaryKey ----
describe('getPrimaryKey', () => {
it('没有标记主键时返回第一列', () => {
const schema = createSchema('test', {
uuid: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 重新构造不带主键标记的 schema
const noPkSchema = { name: 'test', columns: {
first: { type: 'string' as const },
second: { type: 'string' as const },
}};
expect(getPrimaryKey(noPkSchema)).toBe('first');
});
});
// ---- validateRow ----
describe('validateRow', () => {
const schema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
bio: { type: 'string', maxLength: 200 },
score: { type: 'number', min: 0, max: 100, default: 50 },
active: { type: 'boolean', default: true },
birthday: { type: 'date' },
tags: { type: 'json', default: [] },
});
it('跳过 undefined 字段(使用默认值)', () => {
const row = validateRow(schema, { id: '1', name: 'Alice' });
expect(row.id).toBe('1');
expect(row.name).toBe('Alice');
expect(row.score).toBe(50);
expect(row.active).toBe(true);
expect(row.tags).toEqual([]);
});
it('null 值被视为不满足必填', () => {
expect(() => validateRow(schema, { id: '1', name: null })).toThrow('required');
});
it('json 类型接受对象', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: { key: 'value' } });
expect(row.tags).toEqual({ key: 'value' });
});
it('json 类型接受数组', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: [1, 2, 3] });
expect(row.tags).toEqual([1, 2, 3]);
});
it('date 类型接受有效日期字符串', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', birthday: '2024-01-15' });
expect(row.birthday).toBe('2024-01-15');
});
it('date 类型拒绝无效格式', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 'not-a-date' }))
.toThrow('expects valid date');
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 123 }))
.toThrow('expects valid date');
});
it('boolean 类型', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', active: false });
expect(row.active).toBe(false);
});
it('boolean 拒绝非布尔值', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', active: 'yes' }))
.toThrow('expects boolean');
});
it('number 拒绝字符串', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', score: 'high' }))
.toThrow('expects number');
});
it('string 拒绝数字', () => {
expect(() => validateRow(schema, { id: 123, name: 'Alice' }))
.toThrow('expects string');
});
});
// ---- checkFieldType ----
describe('checkFieldType', () => {
it('string 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'string', 'hello')).not.toThrow();
});
it('string 类型拒绝', () => {
expect(() => checkFieldType('t', 'c', 'string', 123)).toThrow('expects string');
});
it('number 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'number', 42)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 3.14)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 0)).not.toThrow();
});
it('boolean 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'boolean', true)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'boolean', false)).not.toThrow();
});
it('json 拒绝非对象', () => {
expect(() => checkFieldType('t', 'c', 'json', 'not-object')).toThrow('expects object');
expect(() => checkFieldType('t', 'c', 'json', 123)).toThrow('expects object');
});
});
// ---- astColumnToColumnDef ----
describe('astColumnToColumnDef', () => {
it('转换基础列', () => {
const result = astColumnToColumnDef({ name: 'id', type: 'string' });
expect(result.type).toBe('string');
});
it('转换带修饰符的列', () => {
const result = astColumnToColumnDef({
name: 'id', type: 'string', primaryKey: true, unique: true, required: true,
});
expect(result.primaryKey).toBe(true);
expect(result.unique).toBe(true);
expect(result.required).toBe(true);
});
});
});
+79
View File
@@ -0,0 +1,79 @@
/**
* Transaction 测试
*/
import { MemoryEngine } from '../../src/engine/memory';
import { TransactionManager, Transaction } from '../../src/transaction/index';
import { createSchema } from '../../src/table/schema';
describe('Transaction', () => {
let engine: MemoryEngine;
let tm: TransactionManager;
const userSchema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
});
beforeEach(async () => {
engine = new MemoryEngine();
await engine.open('test-tx', 1);
await engine.createTable(userSchema);
tm = new TransactionManager(engine);
});
afterEach(async () => {
await engine.close();
});
it('事务中执行插入', async () => {
await tm.execute(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
expect(await engine.count('users')).toBe(2);
});
it('事务中执行混合操作', async () => {
await engine.insert('users', [{ id: '1', name: 'Alice', balance: 100 }]);
await tm.execute(async (trx) => {
await trx.table('users').update({ balance: 200 }).where({ id: '1' }).execute();
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 50 });
});
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
expect(rows[0].balance).toBe(200);
expect(await engine.count('users')).toBe(2);
});
it('事务中的 table() 返回相同实例', async () => {
await tm.execute(async (trx) => {
const t1 = trx.table('users');
const t2 = trx.table('users');
expect(t1).toBe(t2); // 相同表名返回同一实例
});
});
it('Transaction.isCompleted', async () => {
let trxRef: Transaction | null = null;
await tm.execute(async (trx) => {
trxRef = trx;
expect(trx.isCompleted()).toBe(false);
await trx.table('users').insert({ id: '1', name: 'Alice' });
});
expect(trxRef!.isCompleted()).toBe(true);
});
it('事务抛出错误', async () => {
await expect(tm.execute(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice' });
throw new Error('intentional error');
})).rejects.toThrow('Transaction failed');
// 注意:MemoryEngine 没有回滚,所以数据已经写入
// v0.0.1 的 Transaction 只是错误包装
});
});
+99
View File
@@ -0,0 +1,99 @@
/**
* utils.ts 单元测试模板
*/
import {
generateId,
escapeHTML,
debounce,
throttle,
deepMerge,
isBrowser,
} from '../src/utils';
describe('generateId', () => {
test('返回字符串', () => {
expect(typeof generateId()).toBe('string');
});
test('多次调用产生不同值', () => {
const ids = new Set<string>();
for (let i = 0; i < 100; i++) ids.add(generateId());
expect(ids.size).toBe(100);
});
});
describe('escapeHTML', () => {
test('转义 < > &', () => {
const out = escapeHTML('<a>&b</a>');
expect(out).toContain('&lt;');
expect(out).toContain('&gt;');
expect(out).toContain('&amp;');
});
test('null/undefined 返回空字符串', () => {
expect(escapeHTML(null)).toBe('');
expect(escapeHTML(undefined)).toBe('');
});
});
describe('debounce', () => {
test('延迟执行', (done) => {
let called = 0;
const fn = debounce(() => { called++; }, 50);
fn();
expect(called).toBe(0);
setTimeout(() => {
expect(called).toBe(1);
done();
}, 100);
});
test('多次调用只执行最后一次', (done) => {
let result = 0;
const fn = debounce((v: number) => { result = v; }, 50);
fn(1);
fn(2);
fn(3);
setTimeout(() => {
expect(result).toBe(3);
done();
}, 100);
});
});
describe('throttle', () => {
test('首次立即执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
expect(called).toBe(1);
});
test('限流期内不重复执行', () => {
let called = 0;
const fn = throttle(() => { called++; }, 50);
fn();
fn();
fn();
expect(called).toBe(1);
});
});
describe('deepMerge', () => {
test('浅层合并', () => {
const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 });
expect(out).toEqual({ a: 1, b: 3, c: 4 });
});
test('深层对象合并', () => {
const out = deepMerge({ obj: { x: 1 } }, { obj: { y: 2 } });
expect(out).toEqual({ obj: { x: 1, y: 2 } });
});
});
describe('isBrowser', () => {
test('jsdom 环境下返回 true', () => {
expect(isBrowser()).toBe(true);
});
});