Skip to content

Commit db334ac

Browse files
committed
fix(objectql): the having temporal refusal's remedy is where's, naming the placeholder having now resolves
having resolves {placeholder} tokens through where's resolver, so a remedy that names only literal forms steers a caller holding a preset name away from the one spelling that works. The having refusal now reads REMEDY, the table where and the per-aggregation filter read; the parallel HAVING_REMEDY table and its docblock are deleted. DATE_YEAR_REMEDY is untouched, and the time kind's words are the same string as before. The remedy pin is flipped: it asserts code and status (through the shared refusal helper), the message's first sentence, the placeholder named in the remedy, and the remedy equal to the where twin's. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
1 parent c599f75 commit db334ac

2 files changed

Lines changed: 27 additions & 19 deletions

File tree

‎packages/objectql/src/engine-aggregate-having-temporal-door.test.ts‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,32 @@ describe('[#20263] having — a comparand its column cannot read is refused befo
247247
await expectHavingRefusal(() => ({ d: { $gt: Y10000 } }), [{ field: 'opened_at', dateGranularity: 'day', alias: 'd' }]);
248248
});
249249

250-
it('the remedy names the literal forms only, no placeholder', async () => {
251-
for (const having of [{ last_placed: { $lt: 'x' } }, { first_opened: { $lt: 'x' } }]) {
252-
expect(await expectHavingRefusal(() => having)).not.toContain('{30_days_ago}');
250+
// [#20334] `having` resolves placeholders through `where`'s resolver, so its
251+
// remedy is `where`'s: it names the relative-date placeholder that a caller
252+
// holding a preset name (`last_30_days`) needs, on the two kinds that take one.
253+
it('the remedy names the placeholder the resolver knows, in the where twin\'s words', async () => {
254+
const CASES: ReadonlyArray<readonly [Record<string, unknown>, Record<string, unknown>, string]> = [
255+
[{ last_placed: { $lt: 'last_30_days' } }, { placed_on: { $lt: 'last_30_days' } },
256+
"`having` on 'last_placed' (max(placed_on), a date column) compares against \"last_30_days\" at "
257+
+ 'having.last_placed.$lt, which is not a date value this platform can interpret.'],
258+
[{ first_opened: { $lt: 'last_30_days' } }, { opened_at: { $lt: 'last_30_days' } },
259+
"`having` on 'first_opened' (min(opened_at), a datetime column) compares against \"last_30_days\" at "
260+
+ 'having.first_opened.$lt, which is not a datetime value this platform can interpret.'],
261+
];
262+
const after = (message: string, marker: string): string => {
263+
expect(message).toContain(marker);
264+
return message.slice(message.indexOf(marker) + marker.length);
265+
};
266+
for (const [having, where, firstSentence] of CASES) {
267+
// INVALID_FILTER / 400 on both paths, empty or populated, no read.
268+
const message = await expectHavingRefusal(() => having);
269+
expect(message.startsWith(`aggregate('${OBJECT}'): ${firstSentence} `)).toBe(true);
270+
const remedy = after(message, 'The `having` was NOT applied. ');
271+
expect(remedy).toContain('"{30_days_ago}"');
272+
const twin = await whereTwinOf(where);
273+
expect(twin.err?.code).toBe('INVALID_FILTER');
274+
expect(twin.err?.status).toBe(400);
275+
expect(remedy).toBe(after(twin.err!.message, 'The filter was NOT applied. '));
253276
}
254277
});
255278
});

‎packages/objectql/src/temporal-comparand-door.ts‎

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -390,21 +390,6 @@ export function assertTemporalComparandsInterpretable(
390390
);
391391
}
392392

393-
/**
394-
* [#20263] The remedy on a `having` column: the literal forms only, no
395-
* relative-date placeholder, written when `having` resolved none. [#20334]
396-
* `having` resolves placeholders through `where`'s resolver, so
397-
* `{30_days_ago}` is a correct spelling here too; these words were left as
398-
* they were, so no refusal's text moved with that change.
399-
*/
400-
const HAVING_REMEDY: Record<TemporalComparandKind, string> = {
401-
datetime:
402-
'Write an ISO-8601 instant ("2026-07-15T00:00:00.000Z"), a bare "YYYY-MM-DD" '
403-
+ '(read as midnight UTC), or epoch milliseconds.',
404-
date: 'Write a "YYYY-MM-DD" calendar day.',
405-
time: REMEDY.time,
406-
};
407-
408393
/**
409394
* [#20263] Which aggregated column a `having` key names, in the words the
410395
* author wrote it: `max(placed_on)`, `the day bucket of opened_at`, `the
@@ -471,6 +456,6 @@ export function assertHavingTemporalComparandsInterpretable(
471456
`aggregate('${object}'): ${column} compares against ${preview(hit.value)} at ${hit.path}, `
472457
+ `which is not a ${hit.kind} value this platform can interpret. Compared with each group as `
473458
+ 'written, it would keep no group or every group, a 200 indistinguishable from a real answer. '
474-
+ `The \`having\` was NOT applied. ${HAVING_REMEDY[hit.kind]}`,
459+
+ `The \`having\` was NOT applied. ${REMEDY[hit.kind]}`,
475460
);
476461
}

0 commit comments

Comments
 (0)