Skip to content

IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484

Description

@os-zhuang

@objectstack/spec(契约)、driver-sql / driver-memory / driver-mongodb(实现)
发现于:核对 #4419 时(PR #4459)。当时只注意到 Mongo 少了个投影,判定「返回更多数据而非错数据」不属于该 issue 那一类,在代码里注明后留在 PR 之外。后续核查发现问题比那个大。
核实基线main @ ec975f1

摘要

IDataDriver.findStream必填契约方法(不是 optional),文档承诺
「Optimized for large datasets to avoid memory overflow」。实际上:

  1. 全仓没有任何生产调用方。
  2. 三个实现里有两个先把整个结果集读进内存再逐条 yield——正是它声称要避免的事。
  3. 唯一真正流式的那个(Mongo)忽略 query.fields,与同文件的 find 分歧。

这是 declared ≠ enforced(AGENTS.md PD #10)叠加 ADR-0049 enforce-or-remove,
#4480 是同一族。

契约

packages/spec/src/contracts/data-driver.ts:107-112

  /**
   * Stream records matching the structured query.
   * Optimized for large datasets to avoid memory overflow.
   * Returns an AsyncIterable or ReadableStream.
   */
  findStream(object: string, query: QueryAST, options?: DriverOptions): unknown;

必填。所以每个 driver 都得实现,每个测试桩也都得提供一个。

1. 没有调用方

全仓 grep findStream,除了契约声明和三个 driver 自己的实现,其余全部是测试桩
而且多数长这样:

findStream() { throw new Error('not implemented'); }   // engine-unknown-option.test.ts:80
findStream() { throw new Error('ns'); }                // engine-filter-alias.test.ts:60
findStream() { throw new Error('ni'); }                // engine-driver-health.test.ts:20

二十来个测试桩直接抛异常而从没有人注意到——只有在没人调用它时才可能是这样。
引擎没有 stream 入口,REST / 导出 / 批量读路径也都不经过它。

2. 两个实现做的正好相反

SqlDriver.findStreampackages/plugins/driver-sql/src/sql-driver.ts:1494-1504),
它自己的注释就承认了:

  /**
   * Stream records matching a structured query.
   * NOTE: Current implementation fetches all results then yields them.
   * TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
   */
  async *findStream(object, query, options) {
    const results = await this.find(object, query, options);   // ← 整表进内存
    for (const row of results) yield row;
  }

InMemoryDriver.findStreampackages/plugins/driver-memory/src/memory-driver.ts:361-368):同样先 find() 再 yield。

也就是说,一个以「避免内存溢出」为存在理由的方法,在两个 driver 上会先把可能溢出的那份数据完整读进来
如果哪天真有调用方按文档承诺来用它,它会在最需要它的那个规模上失效。

3. 唯一流式的那个丢参数

MongoDBDriver._findStreampackages/plugins/driver-mongodb/src/mongodb-driver.ts:302-323
确实走游标,但:

    const findOptions: FindOptions = {
      session,
      projection: { _id: 0 },     // ← 恒定,忽略 query.fields
    };

同文件的 findbuildFindOptions,会按 query.fields 构造投影。
findStream 不走,所以 fields 在这条路径上被静默丢弃。
#4459findOne 时把 find/findOne 统一到了 buildFindOptions
并在其 TSDoc 里显式记下 _findStream 没有并入——就是这一处。)

要请的是决策,不是补丁

两条路都合理,取决于这个能力还要不要:

enforce —— 接一个真实调用方(导出、批量读是天然位置),并把三个实现补齐:
SQL 走 knex.stream()、内存 driver 真正惰性产出、Mongo 并入 buildFindOptions 拿到 fields 投影。
好处是这个方法终于有人验证;代价是要给它配契约测试,否则同样的分歧还会再长出来。

remove —— 从 IDataDriver 撤掉,删三处实现和二十来个测试桩。
spec-property-retirement 的套路走(tombstone + changeset 里的 FROM → TO)。
如果确实没有产品需求要流式读,这条更诚实——留着一个没人调、且两处实现与文档相反的必填方法,
只会让下一个读到它的人(或 agent)从它推理出错误的结论。

我倾向 remove,除非有明确的大结果集读取需求在排期上。理由是 PD #10 的那句:
一个 case 标签不是执行,调用点才是——这里连调用点都没有,而文档的承诺已经被两个实现反过来了。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions