Skip to content

Commit ba84865

Browse files
committed
fix(spec): align the legacy query options' select and vector-find fields with the projection rule
DataEngineQueryOptionsSchema.select and DataEngineVectorFindRequestSchema.fields take the same array arm (at least one name, none blank) as their engine-side and transport siblings. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfjH9js8WRa8Xi2dmx
1 parent 17ce2df commit ba84865

3 files changed

Lines changed: 45 additions & 11 deletions

File tree

‎.changeset/26954-engine-projection-empty.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
'@objectstack/spec': patch
33
---
44

5-
fix(spec)!: `EngineQueryOptionsSchema.fields` and the RPC request's `select` declare the projection their transport siblings declare, instead of any list (#26954)
5+
fix(spec)!: the engine-side projection declarations (`EngineQueryOptionsSchema.fields`, the RPC request's `select`, the legacy `DataEngineQueryOptionsSchema.select` and the vector-find request's `fields`) declare the projection their transport siblings declare, instead of any list (#26954)
66

7-
Clause-②: no (narrowing) — the published engine-side declarations of a read's projection (`EngineQueryOptionsSchema.fields`, and the deprecated `select` of the RPC query options reached through `DataEngineFindRequestSchema`, `DataEngineFindOneRequestSchema` and `DataEngineRequestSchema`) refuse a list naming no field, which they admitted before. Nothing is widened and no key is added, removed or renamed.
7+
Clause-②: no (narrowing) — the published engine-side declarations of a read's projection (`EngineQueryOptionsSchema.fields`; the deprecated `select` of the RPC query options reached through `DataEngineFindRequestSchema`, `DataEngineFindOneRequestSchema` and `DataEngineRequestSchema`; the deprecated `DataEngineQueryOptionsSchema.select`; and `DataEngineVectorFindRequestSchema.fields`, also an arm of `DataEngineRequestSchema`) refuse a list naming no field, which they admitted before. Nothing is widened and no key is added, removed or renamed.
88
Metadata-protocol: none
99

1010
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no metadata type or stored shape moves: these are engine and RPC request declarations, never stored, and no metadata type embeds them. A caller names at least one field or leaves the projection out; there is nothing for a migration to rewrite and no tombstone. -->
1111

12-
**BREAKING** accept-set narrowing of published declarations (`@objectstack/spec/data`). No runtime in this repository parses either member, so no request served here changes; what can break is code outside it that parses with these exported schemas, or a client generated from their JSON Schema, and sent an empty projection.
12+
**BREAKING** accept-set narrowing of published declarations (`@objectstack/spec/data`). No runtime in this repository parses any of these members, so no request served here changes; what can break is code outside it that parses with these exported schemas, or a client generated from their JSON Schema, and sent an empty projection.
1313

14-
- **Before.** `EngineQueryOptionsSchema.fields` and the RPC query's `select` admitted any list of strings: `[]`, and a list with a blank or whitespace-only name.
15-
- **Now.** A list must hold at least one name, and no name may be blank (JSON Schema `minItems: 1`, and a non-blank pattern on each name). Both read this rule from the same declaration as `QueryTransportParamsSchema.select` / `$select` and `GetDataRequestSchema.select`, so the refusal messages are the same.
14+
- **Before.** `EngineQueryOptionsSchema.fields`, the RPC query's `select`, `DataEngineQueryOptionsSchema.select` and `DataEngineVectorFindRequestSchema.fields` admitted any list of strings: `[]`, and a list with a blank or whitespace-only name.
15+
- **Now.** A list must hold at least one name, and no name may be blank (JSON Schema `minItems: 1`, and a non-blank pattern on each name). All four read this rule from the same declaration as `QueryTransportParamsSchema.select` / `$select` and `GetDataRequestSchema.select`, so the refusal messages are the same.
1616
- **Unchanged.** The inferred types (`string[]`), a named projection, and leaving the projection out. The in-process ObjectQL engine does not parse its options with this schema: `engine.find(object, { fields: [] })` still reads every column, as before.
1717

1818
**The remedy.** Name at least one field, or leave `fields` / `select` out to read every column.

‎packages/spec/src/data/data-engine.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,4 +1311,36 @@ describe('the engine-side projection declarations admit exactly what their trans
13111311
expect(parsed.query).toEqual({ fields: value });
13121312
});
13131313
}
1314+
1315+
// The legacy options bag's `select` and the vector-find request's `fields`
1316+
// are `z.string()` lists, so their sibling is the bare `select`.
1317+
it.each(REFUSED)('DataEngineQueryOptionsSchema.select refuses %s, naming the path, as the bare `select` does', (_name, value, tail) => {
1318+
const parsed = DataEngineQueryOptionsSchema.safeParse({ select: value });
1319+
expect(parsed.success).toBe(false);
1320+
const issue = parsed.success ? undefined : parsed.error.issues[0];
1321+
expect(issue?.path).toEqual(['select', ...tail]);
1322+
expect(issue?.message).toBe(sibling('select', value)?.message);
1323+
});
1324+
1325+
it.each(KEPT)('DataEngineQueryOptionsSchema.select keeps %s, unchanged', (_name, value) => {
1326+
expect(DataEngineQueryOptionsSchema.parse({ select: value }).select).toEqual(value);
1327+
});
1328+
1329+
for (const [label, schema] of [
1330+
['DataEngineVectorFindRequestSchema', DataEngineVectorFindRequestSchema],
1331+
['DataEngineRequestSchema', DataEngineRequestSchema],
1332+
] as const) {
1333+
it.each(REFUSED)(`${label}: vectorFind fields refuses %s, naming the path, as the bare \`select\` does`, (_name, value, tail) => {
1334+
const parsed = schema.safeParse({ method: 'vectorFind', object: 'doc', vector: [0.1, 0.2], fields: value });
1335+
expect(parsed.success).toBe(false);
1336+
const issue = parsed.success ? undefined : parsed.error.issues[0];
1337+
expect(issue?.path).toEqual(['fields', ...tail]);
1338+
expect(issue?.message).toBe(sibling('select', value)?.message);
1339+
});
1340+
1341+
it.each(KEPT)(`${label}: vectorFind fields keeps %s, unchanged`, (_name, value) => {
1342+
const parsed = schema.parse({ method: 'vectorFind', object: 'doc', vector: [0.1, 0.2], fields: value }) as { fields?: string[] };
1343+
expect(parsed.fields).toEqual(value);
1344+
});
1345+
}
13141346
});

‎packages/spec/src/data/data-engine.zod.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ export const BaseEngineOptionsSchema = lazySchema(() => z.object({
9696
* rule cannot drift apart.
9797
*
9898
* [#26954] The engine-side declarations of the same projection —
99-
* `EngineQueryOptionsSchema.fields` and `RpcQueryOptionsSchema.select` — take
100-
* the array arm from here too. It is declared ahead of both because
99+
* `EngineQueryOptionsSchema.fields`, `RpcQueryOptionsSchema.select`, the
100+
* legacy `DataEngineQueryOptionsSchema.select` and
101+
* `DataEngineVectorFindRequestSchema.fields` — take the array arm from here
102+
* too. It is declared ahead of both because
101103
* `RpcQueryOptionsSchema` extends `EngineQueryOptionsSchema` at module load,
102104
* which runs the lazy factory before any later `const` exists.
103105
*/
@@ -212,8 +214,8 @@ export const EngineQueryOptionsSchema = lazySchema(() => BaseEngineOptionsSchema
212214
export const DataEngineQueryOptionsSchema = lazySchema(() => BaseEngineOptionsSchema.extend({
213215
/** @deprecated Use `where` (EngineQueryOptionsSchema) */
214216
filter: DataEngineFilterSchema.optional(),
215-
/** @deprecated Use `fields` (EngineQueryOptionsSchema) */
216-
select: z.array(z.string()).optional(),
217+
/** @deprecated Use `fields` (EngineQueryOptionsSchema) — the same projection rule (#26954) */
218+
select: transportProjectionArray(z.string()).optional(),
217219
/** @deprecated Use `orderBy` (EngineQueryOptionsSchema) */
218220
sort: DataEngineSortSchema.optional(),
219221
limit: z.number().int().min(1).optional(),
@@ -1519,8 +1521,8 @@ export const DataEngineVectorFindRequestSchema = lazySchema(() => z.object({
15191521
vector: z.array(z.number()),
15201522
/** Optional pre-filter (Metadata filtering) — standard QueryAST `where` */
15211523
where: z.union([z.record(z.string(), z.unknown()), FilterConditionSchema]).optional(),
1522-
/** Fields to retrieve — standard QueryAST `fields` */
1523-
fields: z.array(z.string()).optional(),
1524+
/** Fields to retrieve — standard QueryAST `fields`; the same projection rule (#26954) */
1525+
fields: transportProjectionArray(z.string()).optional(),
15241526
/** Number of results */
15251527
limit: z.number().int().default(5).optional(),
15261528
/** Minimum similarity score (0-1) or distance threshold */

0 commit comments

Comments
 (0)