背景(PLAN-v0.7.5.md 根因 2/4):
三个 builder 的 execute() 各自执行写入/查询,与 SQL 路径构成**两条管线**:
- SelectQueryBuilder:无 JOIN 时直接调 engine.find(只有 JOIN 才走 executor)
- UpdateQueryBuilder / DeleteQueryBuilder:直接调 engine.update/delete
于是同一条语义在两条路径上规则各写一份,实测差异:
- `db.table('t').select(['t.n'])` 行键保留 `t.n`,SQL 路径归一化为 `n`
- `select(['nope'])` 静默产出 `[{},{},{}]`(引擎不校验列存在性)
- 不受 maxRowsPerQuery 约束
- UPDATE/DELETE 的 `$subquery`/`$col`/`$exists` 无人解析 → 引擎判 UNKNOWN
→ **静默影响 0 行**(引擎层此前为此加了"检测到未解析标记就抛 NOT_SUPPORTED"
的防御 —— 那是把"管线缺失"暴露成用户错误,方向错了)
改动:
1. SelectQueryBuilder / UpdateQueryBuilder / DeleteQueryBuilder 的 execute()
统一为 `executor.execute(toAST())`;构造函数不再接收 engine。
删掉 `if (joins.length > 0 && executor)` 的分支 —— executor 自己会在安全时
下推到引擎,不需要 builder 代劳。
2. 生命周期钩子(beforeUpdate/afterUpdate/beforeDelete/afterDelete + onWrite
广播)改由 Table 以回调形式注入 builder,顺序与修复前一致
(before → executor → onWrite → after)。回调接收**实际语句**,
因此 beforeUpdate 的 `query.where` 不再是空对象 —— 修复前 builder 路径的
钩子能拿到 where,现在仍然能(新增测试锁定)。
3. Table 新增 requireExecutor():拿不到执行器时**明确报错**,不再静默退化为
"直接调引擎"。Transaction.table() 相应构造绑定同一引擎的 QueryExecutor
(事务原子性仍由引擎的 begin/commit/rollback 提供)。
4. 删除引擎层 4 处 `containsUnresolvedSubqueries → NOT_SUPPORTED` 防御:
写路径已不可能出现未解析标记(builder 与 SQL 都经 Executor),
留着它会让后来者误以为"这里需要防御"。
验证:新增 tests/v080-single-pipeline.test.ts(TABLE API 与 SQL API 逐值等价,
4 引擎 × 12 项 + 跨引擎 1 项,共 57 断言);全量 84 套件 / 1646 测试通过;
typecheck(src+tests) 与 lint 零错误。
206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
/**
|
||
* QueryBuilder 和 Compiler/Executor 测试
|
||
*/
|
||
|
||
import { MemoryEngine } from '../../src/engine/memory';
|
||
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;
|
||
let executor: QueryExecutor;
|
||
|
||
beforeEach(async () => {
|
||
engine = new MemoryEngine();
|
||
await engine.open('test-qb', 1);
|
||
// v0.8.0(B-3):builder 只产出 AST,执行一律经 Executor 唯一管线,
|
||
// 因此构造函数接收 executor 而不是 engine。
|
||
executor = new QueryExecutor(engine);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await engine.close();
|
||
});
|
||
|
||
// ---- SelectQueryBuilder ----
|
||
|
||
describe('SelectQueryBuilder', () => {
|
||
it('基础查询', () => {
|
||
const qb = new SelectQueryBuilder('users', executor);
|
||
expect(qb.toAST()).toMatchObject({
|
||
type: 'SELECT',
|
||
columns: ['*'],
|
||
from: 'users',
|
||
where: {},
|
||
});
|
||
});
|
||
|
||
it('链式调用', () => {
|
||
const qb = new SelectQueryBuilder('users', executor, ['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('users', executor);
|
||
qb.where({ age: { $gt: 18 } }).where({ active: true });
|
||
expect(qb.toAST().where).toEqual({ age: { $gt: 18 }, active: true });
|
||
});
|
||
|
||
it('多次 orderBy 追加排序', () => {
|
||
const qb = new SelectQueryBuilder('users', executor);
|
||
qb.orderBy('age', 'desc').orderBy('name', 'asc');
|
||
expect(qb.toAST().orderBy).toHaveLength(2);
|
||
});
|
||
});
|
||
|
||
// ---- UpdateQueryBuilder ----
|
||
|
||
describe('UpdateQueryBuilder', () => {
|
||
it('基础更新', () => {
|
||
const qb = new UpdateQueryBuilder('users', { age: 31 }, executor);
|
||
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('users', { active: false }, executor);
|
||
expect(qb.toAST().where).toEqual({});
|
||
});
|
||
});
|
||
|
||
// ---- DeleteQueryBuilder ----
|
||
|
||
describe('DeleteQueryBuilder', () => {
|
||
it('基础删除', () => {
|
||
const qb = new DeleteQueryBuilder('users', executor);
|
||
qb.where({ id: '1' });
|
||
const ast = qb.toAST();
|
||
expect(ast).toMatchObject({
|
||
type: 'DELETE',
|
||
from: 'users',
|
||
where: { id: '1' },
|
||
});
|
||
});
|
||
|
||
it('不带 where', () => {
|
||
const qb = new DeleteQueryBuilder('users', executor);
|
||
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);
|
||
});
|
||
});
|