Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/driver-query-signatures-follow-through.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@objectstack/driver-memory": major
"@objectstack/driver-mongodb": major
"@objectstack/driver-sql": major
"@objectstack/driver-sqlite-wasm": major
"@objectstack/driver-turso": major
---

refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075)

#5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit<QueryAST, 'object'>`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。

在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。

收尾之后,「驱动读 `query.object`」直接变成编译错误:

```ts
// 收窄前:编译通过,运行期可能是 undefined —— 谎言
// 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'.
const name = query.object;
```

**零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。

**迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。

标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。

`aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => {

it('stores every writer form as canonical UTC ISO text', async () => {
await seedMixed();
const raw = await driver.find('task', { object: 'task' });
const raw = await driver.find('task', {});
for (const row of raw) {
expect(typeof (row as any).created_at, `${(row as any).id} stored form`).toBe('string');
expect((row as any).created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
Expand Down Expand Up @@ -93,10 +93,10 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => {

it('keeps #3777/#4042 bound semantics on top of the converged storage', async () => {
await seedMixed();
const gte = await driver.find('task', { object: 'task', where: { created_at: { $gte: '2026-07-28' } } });
const gte = await driver.find('task', { where: { created_at: { $gte: '2026-07-28' } } });
expect(ids(gte)).toEqual(['d_midnight', 'd_next_day', 's_evening', 's_morning']);

const lt = await driver.find('task', { object: 'task', where: { created_at: { $lt: '2026-07-28' } } });
const lt = await driver.find('task', { where: { created_at: { $lt: '2026-07-28' } } });
expect(ids(lt)).toEqual(['d_yesterday', 's_old']);

const instant = await driver.find('task', {
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => {
// honest subject for an update-path assertion.
await driver.create('task', { id: 'u1', due_at: '2026-04-19T10:00:00.000Z' });
await driver.update('task', 'u1', { due_at: new Date('2026-07-28T09:15:00Z') });
const row: any = (await driver.find('task', { object: 'task', where: { id: 'u1' } }))[0];
const row: any = (await driver.find('task', { where: { id: 'u1' } }))[0];
expect(row.due_at).toBe('2026-07-28T09:15:00.000Z');

// …and the converged value is reachable by a date window, which is the
Expand All @@ -171,7 +171,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => {
] as const) {
await driver.create('task', { id, created_on });
}
const all = await driver.find('task', { object: 'task' });
const all = await driver.find('task', {});
for (const row of all) expect(typeof (row as any).created_on).toBe('string');
expect((all.find((r: any) => r.id === 'on_obj')).created_on).toBe('2026-07-28');

Expand All @@ -183,7 +183,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => {

it('an undeclared object is left alone (no schema → no coercion)', async () => {
await driver.create('freeform', { id: 'f1', when: new Date('2026-07-28T09:15:00Z') });
const row: any = (await driver.find('freeform', { object: 'freeform' }))[0];
const row: any = (await driver.find('freeform', {}))[0];
expect(row.when).toBeInstanceOf(Date);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ describe('InMemoryDriver — bare-day $lte covers the whole day (#4042)', () =>
});

it('$gte / $gt / $lt keep their midnight anchoring', async () => {
const gte = await driver.find('task', { object: 'task', where: { created_at: { $gte: '2026-07-28' } } });
const gte = await driver.find('task', { where: { created_at: { $gte: '2026-07-28' } } });
expect(ids(gte)).toEqual(['t_evening', 't_midnight', 't_morning']);

const gt = await driver.find('task', { object: 'task', where: { created_at: { $gt: '2026-07-28' } } });
const gt = await driver.find('task', { where: { created_at: { $gt: '2026-07-28' } } });
expect(ids(gt)).toEqual(['t_evening', 't_midnight', 't_morning']); // string '…T00:00' > '2026-07-28'

const lt = await driver.find('task', { object: 'task', where: { created_at: { $lt: '2026-07-28' } } });
const lt = await driver.find('task', { where: { created_at: { $lt: '2026-07-28' } } });
expect(ids(lt)).toEqual(['t_old', 't_yesterday']);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('[#5324] InMemoryDriver.find compiles a document-level $not', () => {
});

const idsFrom = async (driver: InMemoryDriver, where: unknown): Promise<string[]> => {
const rows = await driver.find('deal', { object: 'deal', fields: ['id'], where: where as FilterCondition });
const rows = await driver.find('deal', { fields: ['id'], where: where as FilterCondition });
return (rows as Array<Record<string, unknown>>).map((r) => String(r.id)).sort();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('[#5324] InMemoryDriver.find — filter logic conformance (the LIVE que
});

const ids = async (where: FilterCondition): Promise<string[]> => {
const rows = await driver.find(TABLE, { object: TABLE, fields: ['id'], where });
const rows = await driver.find(TABLE, { fields: ['id'], where });
return (rows as Array<Record<string, unknown>>).map((r) => String(r.id)).sort((x, y) => x.localeCompare(y));
};

Expand Down Expand Up @@ -369,7 +369,7 @@ describe('[#5373] comparand types — the analytics face against the live query
const sorted = (ids: string[]): string[] => [...ids].sort((x, y) => x.localeCompare(y));

const findIds = async (where: FilterCondition): Promise<string[]> => {
const rows = await driver.find(COMPARAND_TABLE, { object: COMPARAND_TABLE, fields: ['id'], where });
const rows = await driver.find(COMPARAND_TABLE, { fields: ['id'], where });
return sorted((rows as Array<Record<string, unknown>>).map((r) => String(r.id)));
};

Expand Down Expand Up @@ -418,7 +418,6 @@ describe('[#5373] comparand types — the analytics face against the live query
*/
it('the fixture stores real booleans and real nulls, not their stringified forms', async () => {
const rows = (await driver.find(COMPARAND_TABLE, {
object: COMPARAND_TABLE,
fields: ['id', 'is_active', 'closed_at', 'code'],
})) as Array<Record<string, unknown>>;
const one = rows.find((r) => r.id === '1')!;
Expand Down Expand Up @@ -606,7 +605,7 @@ describe('[#5374] operator semantics — the analytics face against the live que
const sorted = (ids: string[]): string[] => [...ids].sort((x, y) => x.localeCompare(y));

const findIds = async (where: FilterCondition): Promise<string[]> => {
const rows = await driver.find(COMPARAND_TABLE, { object: COMPARAND_TABLE, fields: ['id'], where });
const rows = await driver.find(COMPARAND_TABLE, { fields: ['id'], where });
return sorted((rows as Array<Record<string, unknown>>).map((r) => String(r.id)));
};

Expand Down
Loading
Loading