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 零错误。
This commit is contained in:
@@ -5,6 +5,7 @@ import { MetonaSqlark } from '../src/core';
|
||||
import { MemoryEngine } from '../src/engine/memory';
|
||||
import { SelectQueryBuilder } from '../src/query/builder';
|
||||
import { createSchema } from '../src/table/schema';
|
||||
import { QueryExecutor } from '../src/query/executor';
|
||||
|
||||
describe('边缘覆盖', () => {
|
||||
let db: MetonaSqlark;
|
||||
@@ -117,18 +118,18 @@ describe('边缘覆盖', () => {
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
it('SelectQueryBuilder.as 别名', () => {
|
||||
const qb = new SelectQueryBuilder(engine, 'users').as('u');
|
||||
const qb = new SelectQueryBuilder('users', new QueryExecutor(engine)).as('u');
|
||||
expect(qb.toAST().alias).toBe('u');
|
||||
});
|
||||
|
||||
it('rightJoin', () => {
|
||||
const qb = new SelectQueryBuilder(engine, 'users');
|
||||
const qb = new SelectQueryBuilder('users', new QueryExecutor(engine));
|
||||
qb.rightJoin('orders', { 'users.id': { $col: 'orders.user_id' } }, 'o');
|
||||
expect(qb.toAST().joins![0].type).toBe('RIGHT');
|
||||
});
|
||||
|
||||
it('join 默认为 INNER', () => {
|
||||
const qb = new SelectQueryBuilder(engine, 'users');
|
||||
const qb = new SelectQueryBuilder('users', new QueryExecutor(engine));
|
||||
qb.join('orders', { 'users.id': { $col: 'orders.user_id' } });
|
||||
expect(qb.toAST().joins![0].type).toBe('INNER');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user