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,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user