Files
MetonaSqlark/tests/query/query-system.test.ts
T
thzxx b20d47bd93 feat(B-3): 单管线 —— QueryBuilder 只产出 AST,执行一律经 Executor
背景(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 零错误。
2026-09-15 00:15:41 +08:00

206 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.0B-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);
});
});