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
+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');
});
});
});