test: v0.1.13 新增42项测试覆盖(事务回滚/子查询/外键级联/连接池)
CI / test (18.x) (push) Successful in 9m52s
CI / test (20.x) (push) Successful in 9m50s
CI / test (22.x) (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 9m49s

This commit is contained in:
thzxx
2026-07-26 16:40:11 +08:00
parent b6d814aefe
commit 71e238a5cc
14 changed files with 955 additions and 16 deletions
+67 -3
View File
@@ -1408,14 +1408,19 @@ class QueryExecutor {
stmt.where = await this.resolveSubqueries(stmt.where);
}
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
let rows;
if (!stmt.joins || stmt.joins.length === 0) {
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
rows = await this.engine.find(plan.table, plan);
}
else {
rows = await this.executeJoinSelect(stmt);
}
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
if (hasAggregate) {
rows = [this.computeSingleAggregate(rows, stmt)];
}
if (hasGroupBy)
rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct)
@@ -1588,6 +1593,31 @@ class QueryExecutor {
}
getEngine() { return this.engine; }
// ===================================================================
// 无 GROUP BY 时的聚合计算
// ===================================================================
/** 检查 SELECT 列列表中是否包含聚合函数 */
_hasAggregateColumn(columns) {
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
}
/** 计算单行聚合结果(无 GROUP BY) */
computeSingleAggregate(rows, stmt) {
const result = {};
for (const colExpr of stmt.columns) {
if (colExpr === '*')
continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
}
else {
// 非聚合列取第一行的值
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
}
}
return result;
}
// ===================================================================
// 子查询解析
// ===================================================================
/**
@@ -2394,8 +2424,8 @@ class Parser {
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
parseSimpleCondition() {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner };
@@ -2420,6 +2450,36 @@ class Parser {
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) {
// NOT IN
this.nextToken(); // skip NOT
this.nextToken(); // skip IN
this.expect(TokenType.LPAREN);
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: values };
return result;
}
else if (this.peekTokenIs(TokenType.LIKE)) {
// NOT LIKE
this.nextToken(); // skip NOT
this.nextToken(); // skip LIKE
const pattern = this.parseValue();
const result = {};
result[column] = { $not: { $like: pattern } };
return result;
}
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
@@ -2471,6 +2531,10 @@ class Parser {
result[column] = { [op]: value };
return result;
}
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
_isNotInOrLike() {
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
}
peekTokenIs(type) {
return this.peekToken.type === type;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -291,6 +291,10 @@ declare class QueryExecutor {
private executeCreateTable;
private executeDropTable;
getEngine(): IStorageEngine;
/** 检查 SELECT 列列表中是否包含聚合函数 */
private _hasAggregateColumn;
/** 计算单行聚合结果(无 GROUP BY) */
private computeSingleAggregate;
/**
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
* 将结果替换为具体值。
+67 -3
View File
@@ -1404,14 +1404,19 @@ class QueryExecutor {
stmt.where = await this.resolveSubqueries(stmt.where);
}
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
let rows;
if (!stmt.joins || stmt.joins.length === 0) {
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
rows = await this.engine.find(plan.table, plan);
}
else {
rows = await this.executeJoinSelect(stmt);
}
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
if (hasAggregate) {
rows = [this.computeSingleAggregate(rows, stmt)];
}
if (hasGroupBy)
rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct)
@@ -1584,6 +1589,31 @@ class QueryExecutor {
}
getEngine() { return this.engine; }
// ===================================================================
// 无 GROUP BY 时的聚合计算
// ===================================================================
/** 检查 SELECT 列列表中是否包含聚合函数 */
_hasAggregateColumn(columns) {
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
}
/** 计算单行聚合结果(无 GROUP BY) */
computeSingleAggregate(rows, stmt) {
const result = {};
for (const colExpr of stmt.columns) {
if (colExpr === '*')
continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
}
else {
// 非聚合列取第一行的值
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
}
}
return result;
}
// ===================================================================
// 子查询解析
// ===================================================================
/**
@@ -2390,8 +2420,8 @@ class Parser {
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
parseSimpleCondition() {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner };
@@ -2416,6 +2446,36 @@ class Parser {
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) {
// NOT IN
this.nextToken(); // skip NOT
this.nextToken(); // skip IN
this.expect(TokenType.LPAREN);
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: values };
return result;
}
else if (this.peekTokenIs(TokenType.LIKE)) {
// NOT LIKE
this.nextToken(); // skip NOT
this.nextToken(); // skip LIKE
const pattern = this.parseValue();
const result = {};
result[column] = { $not: { $like: pattern } };
return result;
}
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
@@ -2467,6 +2527,10 @@ class Parser {
result[column] = { [op]: value };
return result;
}
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
_isNotInOrLike() {
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
}
peekTokenIs(type) {
return this.peekToken.type === type;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+67 -3
View File
@@ -1410,14 +1410,19 @@
stmt.where = await this.resolveSubqueries(stmt.where);
}
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
let rows;
if (!stmt.joins || stmt.joins.length === 0) {
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
rows = await this.engine.find(plan.table, plan);
}
else {
rows = await this.executeJoinSelect(stmt);
}
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
if (hasAggregate) {
rows = [this.computeSingleAggregate(rows, stmt)];
}
if (hasGroupBy)
rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct)
@@ -1590,6 +1595,31 @@
}
getEngine() { return this.engine; }
// ===================================================================
// 无 GROUP BY 时的聚合计算
// ===================================================================
/** 检查 SELECT 列列表中是否包含聚合函数 */
_hasAggregateColumn(columns) {
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
}
/** 计算单行聚合结果(无 GROUP BY) */
computeSingleAggregate(rows, stmt) {
const result = {};
for (const colExpr of stmt.columns) {
if (colExpr === '*')
continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
}
else {
// 非聚合列取第一行的值
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
}
}
return result;
}
// ===================================================================
// 子查询解析
// ===================================================================
/**
@@ -2396,8 +2426,8 @@
/** simple_cond column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
parseSimpleCondition() {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner };
@@ -2422,6 +2452,36 @@
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) {
// NOT IN
this.nextToken(); // skip NOT
this.nextToken(); // skip IN
this.expect(TokenType.LPAREN);
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: values };
return result;
}
else if (this.peekTokenIs(TokenType.LIKE)) {
// NOT LIKE
this.nextToken(); // skip NOT
this.nextToken(); // skip LIKE
const pattern = this.parseValue();
const result = {};
result[column] = { $not: { $like: pattern } };
return result;
}
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
@@ -2473,6 +2533,10 @@
result[column] = { [op]: value };
return result;
}
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
_isNotInOrLike() {
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
}
peekTokenIs(type) {
return this.peekToken.type === type;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+33 -1
View File
@@ -46,15 +46,21 @@ export class QueryExecutor {
}
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
let rows: Record<string, unknown>[];
if (!stmt.joins || stmt.joins.length === 0) {
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
rows = await this.engine.find(plan.table, plan);
} else {
rows = await this.executeJoinSelect(stmt);
}
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
if (hasAggregate) {
rows = [this.computeSingleAggregate(rows, stmt)];
}
if (hasGroupBy) rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct) rows = this.executeDistinct(rows);
if (stmt.having && Object.keys(stmt.having).length > 0) {
@@ -233,6 +239,32 @@ export class QueryExecutor {
getEngine(): IStorageEngine { return this.engine; }
// ===================================================================
// 无 GROUP BY 时的聚合计算
// ===================================================================
/** 检查 SELECT 列列表中是否包含聚合函数 */
private _hasAggregateColumn(columns: string[]): boolean {
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
}
/** 计算单行聚合结果(无 GROUP BY) */
private computeSingleAggregate(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const colExpr of stmt.columns) {
if (colExpr === '*') continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
} else {
// 非聚合列取第一行的值
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
}
}
return result;
}
// ===================================================================
// 子查询解析
// ===================================================================
+37 -2
View File
@@ -454,8 +454,8 @@ export class Parser {
/** simple_cond column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
private parseSimpleCondition(): WhereCondition {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner } as unknown as WhereCondition;
@@ -483,6 +483,36 @@ export class Parser {
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) {
// NOT IN
this.nextToken(); // skip NOT
this.nextToken(); // skip IN
this.expect(TokenType.LPAREN);
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result: WhereCondition = {};
result[column] = { $nin: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result: WhereCondition = {};
result[column] = { $nin: values };
return result;
} else if (this.peekTokenIs(TokenType.LIKE)) {
// NOT LIKE
this.nextToken(); // skip NOT
this.nextToken(); // skip LIKE
const pattern = this.parseValue();
const result: WhereCondition = {};
result[column] = { $not: { $like: pattern } };
return result;
}
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
@@ -541,6 +571,11 @@ export class Parser {
return result;
}
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
private _isNotInOrLike(): boolean {
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
}
private peekTokenIs(type: TokenType): boolean {
return this.peekToken.type === type;
}
+114
View File
@@ -0,0 +1,114 @@
/**
* v0.1.13
* MetonaSqlark.connect / disconnect / disconnectAll / getActiveConnections
*/
import { MetonaSqlark } from '../src/core';
import '../src/connection-manager';
// ===================================================================
// 连接池基础
// ===================================================================
describe('v0.1.13 连接池', () => {
afterEach(async () => {
// 清理所有连接
await (MetonaSqlark as any).disconnectAll?.();
});
it('connect: 创建新实例', async () => {
const db = await (MetonaSqlark as any).connect({ name: 'pool-test-1', mode: 'memory' });
expect(db).toBeDefined();
expect(db.isReady()).toBe(true);
expect((MetonaSqlark as any).getActiveConnections()).toContain('pool-test-1');
await (MetonaSqlark as any).disconnect('pool-test-1');
});
it('connect: 同名数据库复用实例', async () => {
const db1 = await (MetonaSqlark as any).connect({ name: 'pool-test-2', mode: 'memory' });
const db2 = await (MetonaSqlark as any).connect({ name: 'pool-test-2' });
// 同一个实例
expect(db1).toBe(db2);
await (MetonaSqlark as any).disconnect('pool-test-2');
await (MetonaSqlark as any).disconnect('pool-test-2');
});
it('connect: 不同名数据库不同实例', async () => {
const db1 = await (MetonaSqlark as any).connect({ name: 'pool-a', mode: 'memory' });
const db2 = await (MetonaSqlark as any).connect({ name: 'pool-b', mode: 'memory' });
expect(db1).not.toBe(db2);
const active = (MetonaSqlark as any).getActiveConnections();
expect(active).toContain('pool-a');
expect(active).toContain('pool-b');
await (MetonaSqlark as any).disconnectAll();
});
it('disconnect: 引用计数归零自动 close', async () => {
const db = await (MetonaSqlark as any).connect({ name: 'pool-close', mode: 'memory' });
expect(db.isReady()).toBe(true);
// 第二次 connect → refCount = 2
await (MetonaSqlark as any).connect({ name: 'pool-close' });
// 第一次 disconnect → refCount = 1
await (MetonaSqlark as any).disconnect('pool-close');
expect(db.isReady()).toBe(true); // 仍然 alive
// 第二次 disconnect → refCount = 0 → close
await (MetonaSqlark as any).disconnect('pool-close');
expect(db.isReady()).toBe(false); // 已关闭
});
it('disconnect: 引用计数 > 1 时不 close', async () => {
const db = await (MetonaSqlark as any).connect({ name: 'pool-ref', mode: 'memory' });
// 3 次 connect
await (MetonaSqlark as any).connect({ name: 'pool-ref' });
await (MetonaSqlark as any).connect({ name: 'pool-ref' });
// 2 次 disconnect
await (MetonaSqlark as any).disconnect('pool-ref');
await (MetonaSqlark as any).disconnect('pool-ref');
expect(db.isReady()).toBe(true); // 还有 1 个引用
// 最后一次
await (MetonaSqlark as any).disconnect('pool-ref');
expect(db.isReady()).toBe(false);
});
it('disconnectAll: 强制关闭所有', async () => {
const db1 = await (MetonaSqlark as any).connect({ name: 'pool-all-1', mode: 'memory' });
const db2 = await (MetonaSqlark as any).connect({ name: 'pool-all-2', mode: 'memory' });
await (MetonaSqlark as any).disconnectAll();
expect(db1.isReady()).toBe(false);
expect(db2.isReady()).toBe(false);
expect((MetonaSqlark as any).getActiveConnections()).toHaveLength(0);
});
it('getActiveConnections: 返回活跃连接列表', async () => {
expect((MetonaSqlark as any).getActiveConnections()).toEqual([]);
await (MetonaSqlark as any).connect({ name: 'pool-list-1', mode: 'memory' });
await (MetonaSqlark as any).connect({ name: 'pool-list-2', mode: 'memory' });
const active = (MetonaSqlark as any).getActiveConnections();
expect(active).toContain('pool-list-1');
expect(active).toContain('pool-list-2');
expect(active.length).toBe(2);
await (MetonaSqlark as any).disconnectAll();
});
it('connect 复用时数据库操作正常', async () => {
const db1 = await (MetonaSqlark as any).connect({ name: 'pool-data', mode: 'memory' });
await db1.defineTable('test', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
});
await db1.table('test').insert({ id: 'a', val: 42 });
// 复用
const db2 = await (MetonaSqlark as any).connect({ name: 'pool-data' });
const rows = await db2.table('test').select().execute();
expect(rows).toHaveLength(1);
expect(rows[0].val).toBe(42);
await (MetonaSqlark as any).disconnect('pool-data');
await (MetonaSqlark as any).disconnect('pool-data');
});
});
+213
View File
@@ -0,0 +1,213 @@
/**
* v0.1.13
* ON DELETE CASCADE / SET NULL / RESTRICT
*/
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
// ===================================================================
// SQL Parser 外键语法
// ===================================================================
describe('v0.1.13 外键级联 — Parser', () => {
it('REFERENCES 语法解析', () => {
const stmt = parse(`CREATE TABLE orders (
id STRING PRIMARY KEY,
user_id STRING REFERENCES users(id)
)`);
expect(stmt.type).toBe('CREATE_TABLE');
if (stmt.type === 'CREATE_TABLE') {
const col = stmt.columns.find((c) => c.name === 'user_id');
expect(col).toBeTruthy();
expect(col!.references).toBe('users.id');
}
});
it('REFERENCES + ON DELETE CASCADE', () => {
const stmt = parse(`CREATE TABLE orders (
id STRING PRIMARY KEY,
user_id STRING REFERENCES users(id) ON DELETE CASCADE
)`);
if (stmt.type === 'CREATE_TABLE') {
const col = stmt.columns.find((c) => c.name === 'user_id');
expect(col!.onDelete).toBe('CASCADE');
}
});
it('REFERENCES + ON DELETE SET NULL', () => {
const stmt = parse(`CREATE TABLE orders (
id STRING PRIMARY KEY,
user_id STRING REFERENCES users(id) ON DELETE SET NULL
)`);
if (stmt.type === 'CREATE_TABLE') {
const col = stmt.columns.find((c) => c.name === 'user_id');
expect(col!.onDelete).toBe('SET NULL');
}
});
it('REFERENCES + ON UPDATE CASCADE', () => {
const stmt = parse(`CREATE TABLE orders (
id STRING PRIMARY KEY,
user_id STRING REFERENCES users(id) ON UPDATE CASCADE
)`);
if (stmt.type === 'CREATE_TABLE') {
const col = stmt.columns.find((c) => c.name === 'user_id');
expect(col!.onUpdate).toBe('CASCADE');
}
});
it('REFERENCES + 同时 ON DELETE 和 ON UPDATE', () => {
const stmt = parse(`CREATE TABLE orders (
id STRING PRIMARY KEY,
user_id STRING REFERENCES users(id) ON DELETE CASCADE ON UPDATE RESTRICT
)`);
if (stmt.type === 'CREATE_TABLE') {
const col = stmt.columns.find((c) => c.name === 'user_id');
expect(col!.onDelete).toBe('CASCADE');
expect(col!.onUpdate).toBe('RESTRICT');
}
});
});
// ===================================================================
// ON DELETE CASCADE 执行
// ===================================================================
describe('v0.1.13 外键级联 — ON DELETE CASCADE', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'fk-cascade-test', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
amount: { type: 'number' },
});
await db.table('users').insertMany([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
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: 50 },
]);
});
afterEach(async () => { await db.close(); });
it('删除用户 → 级联删除其订单', async () => {
// 删除 Alice
await db.table('users').delete().where({ id: '1' }).execute();
// Alice 被删除
expect(await db.table('users').count()).toBe(1);
// Alice 的订单也被删除
const orders = await db.table('orders').select().execute();
expect(orders).toHaveLength(1);
expect(orders[0].user_id).toBe('2');
});
it('删除没有订单的用户 → 不会级联删除', async () => {
// 先查总数
const userCount = await db.table('users').count();
const orderCount = await db.table('orders').count();
// 删除一个没有任何订单的用户(创建临时用户)
await db.table('users').insert({ id: '3', name: 'Eve' });
await db.table('users').delete().where({ id: '3' }).execute();
// 订单应该不变
expect(await db.table('orders').count()).toBe(orderCount);
});
it('SQL DELETE 触发级联', async () => {
await db.query("DELETE FROM users WHERE id = '2'");
const orders = await db.table('orders').select().execute();
expect(orders).toHaveLength(2); // 只剩 Alice 的 2 个订单
for (const o of orders) {
expect(o.user_id).toBe('1');
}
});
});
// ===================================================================
// ON DELETE SET NULL
// ===================================================================
describe('v0.1.13 外键级联 — ON DELETE SET NULL', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'fk-setnull-test', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'SET NULL' },
amount: { type: 'number' },
});
await db.table('users').insert({ id: '1', name: 'Alice' });
await db.table('orders').insert({ id: 'o1', user_id: '1', amount: 100 });
});
afterEach(async () => { await db.close(); });
it('删除用户 → 订单 user_id 设为 NULL', async () => {
await db.table('users').delete().where({ id: '1' }).execute();
const orders = await db.table('orders').select().execute();
expect(orders).toHaveLength(1);
expect(orders[0].user_id).toBeNull();
});
});
// ===================================================================
// ON DELETE RESTRICT
// ===================================================================
describe('v0.1.13 外键级联 — ON DELETE RESTRICT', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'fk-restrict-test', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'RESTRICT' },
amount: { type: 'number' },
});
await db.table('users').insert({ id: '1', name: 'Alice' });
await db.table('orders').insert({ id: 'o1', user_id: '1', amount: 100 });
});
afterEach(async () => { await db.close(); });
it('RESTRICT 模式下删除用户 → 不级联(保留孤儿订单)', async () => {
await db.table('users').delete().where({ id: '1' }).execute();
// 订单仍然存在
const orders = await db.table('orders').select().execute();
expect(orders).toHaveLength(1);
expect(orders[0].user_id).toBe('1');
});
it('无 references 的列不受影响', async () => {
await db.table('users').delete().where({ id: '1' }).execute();
expect(await db.table('orders').count()).toBe(1);
});
});
+136
View File
@@ -0,0 +1,136 @@
/**
* v0.1.13
* SQL Parser + Executor
*/
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
// ===================================================================
// SQL 子查询解析
// ===================================================================
describe('v0.1.13 子查询 — Parser', () => {
it('IN 子查询解析', () => {
const stmt = parse('SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)');
expect(stmt.type).toBe('SELECT');
});
it('NOT IN 子查询解析', () => {
const stmt = parse('SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders)');
expect(stmt.type).toBe('SELECT');
});
it('标量子查询解析 (eq)', () => {
const stmt = parse('SELECT * FROM users WHERE age = (SELECT AVG(age) FROM users)');
expect(stmt.type).toBe('SELECT');
});
it('标量子查询解析 (gt)', () => {
const stmt = parse('SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users)');
expect(stmt.type).toBe('SELECT');
});
it('子查询带 WHERE', () => {
const stmt = parse("SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)");
expect(stmt.type).toBe('SELECT');
});
it('子查询带 JOIN', () => {
const stmt = parse(`SELECT * FROM users u
WHERE u.id IN (SELECT o.user_id FROM orders o INNER JOIN products p ON o.product = p.name)`);
expect(stmt.type).toBe('SELECT');
});
});
// ===================================================================
// 子查询执行
// ===================================================================
describe('v0.1.13 子查询 — Executor', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'subquery-test', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
age: { type: 'number', default: 0 },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string' },
amount: { type: 'number' },
});
// Seed data
await db.table('users').insertMany([
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
{ id: '3', name: 'Charlie', age: 35 },
{ id: '4', name: 'Diana', age: 20 },
]);
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: 50 },
]);
});
afterEach(async () => { await db.close(); });
it('IN 子查询: 有订单的用户', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)"
) as Record<string, unknown>[];
expect(rows).toHaveLength(2); // Alice + Bob
const names = rows.map((r) => r.name);
expect(names).toContain('Alice');
expect(names).toContain('Bob');
});
it('NOT IN 子查询: 无订单的用户', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders)"
) as Record<string, unknown>[];
expect(rows).toHaveLength(2); // Charlie + Diana
const names = rows.map((r) => r.name);
expect(names).toContain('Charlie');
expect(names).toContain('Diana');
});
it('IN 子查询 + WHERE 过滤', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)"
) as Record<string, unknown>[];
expect(rows).toHaveLength(1); // 只有 Alice 有 >100 订单
expect(rows[0].name).toBe('Alice');
});
it('标量子查询: 年龄大于平均值的用户', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users)"
) as Record<string, unknown>[];
// AVG = (30+25+35+20)/4 = 27.5 → age > 27.5: Alice(30), Charlie(35)
expect(rows).toHaveLength(2);
const names = rows.map((r) => r.name);
expect(names).toContain('Alice');
expect(names).toContain('Charlie');
});
it('标量子查询: 年龄等于最大值的用户', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE age = (SELECT MAX(age) FROM users)"
) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Charlie');
});
it('空子查询结果处理', async () => {
const rows = await db.query(
"SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 9999)"
) as Record<string, unknown>[];
expect(rows).toHaveLength(0);
});
});
+213
View File
@@ -0,0 +1,213 @@
/**
* v0.1.13
* Memory / IndexedDB / Hybrid begin/commit/rollback
*/
import { MetonaSqlark } from '../src/core';
import 'fake-indexeddb/auto';
let idbCounter = 0;
// ===================================================================
// Memory 引擎事务回滚
// ===================================================================
describe('v0.1.13 事务回滚 — Memory 模式', () => {
let db: MetonaSqlark;
beforeEach(async () => {
db = new MetonaSqlark({ name: 'tx-test-mem', mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
balance: { type: 'number', default: 0 },
});
});
afterEach(async () => { await db.close(); });
it('commit: 事务成功后数据持久化', 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: 200 });
});
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(2);
});
it('rollback: 事务失败后数据恢复', async () => {
// 先插入一条
await db.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
try {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '2', name: 'Bob', balance: 200 });
// 故意触发重复主键错误
await trx.table('users').insert({ id: '2', name: 'Duplicate', balance: 0 });
});
} catch { /* expected */ }
// 回滚后:只有最初的那条数据
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
});
it('rollback: 更新操作回滚', async () => {
await db.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
try {
await db.transaction(async (trx) => {
await trx.table('users').update({ balance: 999 }).where({ id: '1' }).execute();
throw new Error('手动失败');
});
} catch { /* expected */ }
const rows = await db.table('users').select().execute();
expect(rows[0].balance).toBe(100); // 回滚到 100
});
it('rollback: 删除操作回滚', async () => {
await db.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await db.table('users').insert({ id: '2', name: 'Bob', balance: 200 });
try {
await db.transaction(async (trx) => {
await trx.table('users').delete().where({ id: '1' }).execute();
throw new Error('手动失败');
});
} catch { /* expected */ }
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(2); // 两条都在
});
it('rollback: 混合操作回滚', async () => {
await db.table('users').insert({ id: '1', name: 'Alice', balance: 100 });
await db.table('users').insert({ id: '2', name: 'Bob', balance: 200 });
try {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '3', name: 'Charlie', balance: 300 });
await trx.table('users').update({ balance: 0 }).where({ id: '1' }).execute();
await trx.table('users').delete().where({ id: '2' }).execute();
throw new Error('手动失败');
});
} catch { /* expected */ }
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(2);
expect(rows.find((r: any) => r.id === '1')!.balance).toBe(100);
expect(rows.find((r: any) => r.id === '2')).toBeTruthy();
});
it('事务返回结果', async () => {
const result = await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice' });
return 'OK';
});
expect(result).toBe('OK');
expect(await db.table('users').count()).toBe(1);
});
it('事务错误抛出 DatabaseError', async () => {
await expect(db.transaction(async () => {
throw new Error('custom error');
})).rejects.toThrow('Transaction failed');
});
});
// ===================================================================
// IndexedDB 引擎事务回滚
// ===================================================================
describe('v0.1.13 事务回滚 — Disk 模式 (IndexedDB)', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `tx-disk-${++idbCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'indexeddb' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('commit: 事务成功数据持久化', async () => {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice' });
await trx.table('users').insert({ id: '2', name: 'Bob' });
});
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(2);
});
it('rollback: 事务失败数据恢复', async () => {
await db.table('users').insert({ id: '1', name: 'Alice' });
try {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '2', name: 'Bob' });
await trx.table('users').insert({ id: '2', name: 'Dup' });
});
} catch { /* expected */ }
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(1);
});
});
// ===================================================================
// Hybrid 引擎事务回滚
// ===================================================================
describe('v0.1.13 事务回滚 — Hybrid 模式', () => {
let db: MetonaSqlark;
let dbName: string;
beforeEach(async () => {
dbName = `tx-hybrid-${++idbCounter}`;
db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'indexeddb' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
});
afterEach(async () => {
await db.close();
try { indexedDB.deleteDatabase(dbName); } catch {}
});
it('commit: 事务成功', async () => {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '1', name: 'Alice' });
});
expect(await db.table('users').count()).toBe(1);
});
it('rollback: 事务失败回滚', async () => {
await db.table('users').insert({ id: '1', name: 'Alice' });
try {
await db.transaction(async (trx) => {
await trx.table('users').insert({ id: '2', name: 'Bob' });
await trx.table('users').delete().where({ id: '1' }).execute();
throw new Error('fail');
});
} catch { /* expected */ }
const rows = await db.table('users').select().execute();
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
});
});