Skip to content

Commit 3129311

Browse files
committed
fix(objectql,metadata-protocol)!: per-aggregation filter takes where's remaining doors
The per-aggregation filter now runs the temporal-comparand door, judges a { $field } referent and an addDays pair against the object's declared fields (withheld words, as where gets for the same comparison), compares a Date bound as an instant, and the REST door judges its keys with where's unknown-field gate. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 49144fc commit 3129311

3 files changed

Lines changed: 308 additions & 29 deletions

File tree

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10509,6 +10509,19 @@ export class ObjectStackProtocolImplementation implements
1050910509
* it answered `null`/`0` while looking like a served query. `count` with
1051010510
* no field (or the explicit `'*'` sentinel) is the one legitimate
1051110511
* field-less form and passes.
10512+
*
10513+
* [#20148] …and the KEYS inside each entry's `filter` (the per-aggregation
10514+
* filter, `AggregationNodeSchema.filter`), judged by the very gate the
10515+
* explicit `where` takes ({@link assertFilterFieldsExist}): the same field
10516+
* set, the same `unknown` > `dotted` > virtual ladder, the same
10517+
* `INVALID_FIELD` / 400, with the caller's own position as the parameter
10518+
* (`aggregations[1].filter`). Measured on the base through `POST
10519+
* /data/:object/query` on driver-memory and driver-sql: `{ nope: 1 }` in
10520+
* one aggregation's filter answered 200 with that count 0 (every count
10521+
* under `$ne`, a `$not`, or a `$or` branch that held), while the same key
10522+
* in `where` answered this 400. Run after the entry and field checks
10523+
* above, so an entry the spec cannot read keeps its shape verdict, and an
10524+
* unknown aggregated field keeps its own.
1051210525
*/
1051310526
private assertAggregationFieldsExist(object: string, aggregations: unknown): void {
1051410527
if (aggregations === undefined || aggregations === null) return;
@@ -10597,7 +10610,15 @@ export class ObjectStackProtocolImplementation implements
1059710610
const gate = this.resolveQueryFields(object);
1059810611
if (!gate) return;
1059910612
const unknown = fieldsToCheck.filter((f) => !gate.known.has(f));
10600-
if (unknown.length === 0) return;
10613+
if (unknown.length === 0) {
10614+
// [#20148] The filter keys, entry by entry, through `where`'s own
10615+
// gate — see this method's doc. A filter that is not a plain object
10616+
// names no key here and is left to the engine's shape gate.
10617+
aggregations.forEach((entry, i) => {
10618+
this.assertFilterFieldsExist(object, entry.filter, `aggregations[${i}].filter`);
10619+
});
10620+
return;
10621+
}
1060110622
const first = unknown[0];
1060210623
const dottedHint = first.includes('.') && gate.known.has(first.split('.')[0])
1060310624
? " Aggregation runs over this object's own columns; a related record's column cannot "

‎packages/objectql/src/engine.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16141,6 +16141,18 @@ export class ObjectQL implements IObjectQLEngine {
1614116141
// spoke on `where` alone would answer one mistake two ways within a
1614216142
// single verb.
1614316143
assertTextOperatorTargetsAreStringCapable(object, 'aggregate', this._registry.getObject(object), aggFilter);
16144+
// [#20148] …and the TEMPORAL-comparand door, fourth here as it is
16145+
// fourth on `where`'s seam (#8690) — the same function, against the
16146+
// object's declared fields, since this filter reads the object's
16147+
// raw rows. A comparand a declared date / datetime / time field
16148+
// cannot interpret (`'not-a-date'`, the preset name
16149+
// `'last_30_days'`) was compared as written: no row counted, a
16150+
// `$between` with one such endpoint counted EVERY row, and the same
16151+
// bound in a `where` was refused 400 — measured on the base on
16152+
// driver-memory and driver-sql, through the engine and REST.
16153+
// Before the token resolver below, as there, so a `{placeholder}`
16154+
// is stepped around and resolved (or refused) a moment later.
16155+
assertTemporalComparandsInterpretable(object, 'aggregate', this._registry.getObject(object), aggFilter);
1614416156
// [#20122] …and the two doors `having` took at its own entry
1614516157
// (#20099), so a refusal here is the FILTER's, never the data's:
1614616158
// 1. the comparand-TYPE door `where` takes in
@@ -16155,8 +16167,23 @@ export class ObjectQL implements IObjectQLEngine {
1615516167
// walks the filter per SOURCE row, so `{ amount: { $median: 1 } }`
1615616168
// was a 400 on a populated table and a `200 []` on an empty one,
1615716169
// and a `$or` whose first branch held counted every row.
16170+
// [#20148] …then, against the object's DECLARED fields, the two
16171+
// cross-field rules `where` gets from `driver-sql`'s compiler:
16172+
// a `{ $field }` names a declared field, and an `addDays` pair
16173+
// follows `FieldReferenceSchema.addDays`' class rule. Both were
16174+
// answered by the fallback — no row counted — where `where` is
16175+
// refused. Refused in `where`'s words for that comparison, which
16176+
// withhold the fields, the operator and the reason; the
16177+
// withheld half goes to this log, as the driver writes its own.
1615816178
const typed = normalizeFilterComparandTypes(aggFilter, `aggregate('${object}')`, `aggregations[${i}].filter`);
16159-
assertAggregationFilterIsEvaluable(typed, i);
16179+
assertAggregationFilterIsEvaluable(typed, i, {
16180+
object,
16181+
fields: (this._registry.getObject(object) as { fields?: unknown } | undefined)?.fields,
16182+
reportWithheld: (diagnostic) => this.logger.warn(
16183+
`aggregate('${object}'): INVALID_FILTER — refusal detail withheld from the response, as it `
16184+
+ `is for the same cross-field comparison in a where. Full diagnostic: ${diagnostic}`,
16185+
),
16186+
});
1616016187
if (typed !== aggFilter) {
1616116188
typedAggregations ??= [...aggregations];
1616216189
typedAggregations[i] = { ...(agg as object), filter: typed } as typeof agg;

0 commit comments

Comments
 (0)