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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions