- 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%覆盖率 - 零运行时依赖
210 lines
7.1 KiB
TypeScript
210 lines
7.1 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|
||
});
|