Skip to content

Commit 8b950b8

Browse files
committed
chore(changeset): state the having remedy change and its REST bound reading; correct the #20263 note again
20334-aggregate-positions: a paragraph states that the having temporal refusal's remedy is now where's, and re-measures the REST 500-character bound for the having refusals that remedy reaches (date: whole in every measured cell, at most 90 characters of names and path; datetime: always cut inside the remedy, as where's datetime refusal already was). The "keeps that refusal and its words" and "Unchanged" sentences are narrowed to match. 20263-having-temporal-comparand-door (DELIBERATE CORRECTION): the clause "...and the refusal's remedy names none" now says the date / datetime remedy is where's and names the placeholders, and the Fix line gains the placeholder form. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
1 parent db334ac commit 8b950b8

2 files changed

Lines changed: 6 additions & 4 deletions

File tree

‎.changeset/20263-having-temporal-comparand-door.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ What is judged:
2828

2929
- The same walk and the same predicate, `isUninterpretableTemporalComparand` in `@objectstack/core`, that the door runs on `where` and on each per-aggregation `filter`. A change to that rule reaches `having` with it.
3030
- The kind is the aggregated column's class, the one the `addDays` rule already reads: `min` / `max` of a `date`, `datetime` or `time` field keeps that kind, a groupBy projection of such a field takes its kind, and a `day` bucket is a `date`. `count`, `count_distinct`, `sum` and `avg`, a `week` / `month` / `quarter` / `year` bucket, and every other column are not temporal, so they are not judged.
31-
- Every comparison and set operator's comparand, each `$in` / `$nin` member and `$between` endpoint, and the implicit-equality slot, under `$and`, `$or` and `$not`. As on `where`, a `{placeholder}` string, the empty string, `null` and a `{ $field }` reference are not judged. `having` resolves placeholders from the same release (#20334), after this door, and the refusal's remedy names none.
31+
- Every comparison and set operator's comparand, each `$in` / `$nin` member and `$between` endpoint, and the implicit-equality slot, under `$and`, `$or` and `$not`. As on `where`, a `{placeholder}` string, the empty string, `null` and a `{ $field }` reference are not judged. `having` resolves placeholders from the same release (#20334), after this door, so the refusal's remedy on a `date` or `datetime` column is the `where` refusal's and names them, e.g. `{30_days_ago}` / `{current_month_start}`.
3232
- The text operators (`$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$icontains`) are not judged. On `where` the text-operator declared-type door answers them first, and that door does not front `having`.
3333
- The door runs after every other `having` door, so a clause one of them refuses (an unknown operator, a key naming no column, a comparand of no comparable type, an `addDays` pair, an array in the equality slot) keeps that refusal and its words.
3434

3535
The refusal follows the `where` door's words. It names the `having` path, the column, what the column aggregates and its kind, and the comparand, for example: `` `having` on 'last_placed' (max(placed_on), a date column) compares against "not-a-date" at having.last_placed.$lt ``. Like the `where` refusal, it names the column's kind, and otherwise only what the query carries.
3636

3737
**Who is affected.** `having` is a request-only key (`QuerySchema.having`, `EngineAggregateOptions.having`), and no metadata type stores it. Every `having` in this repository's docs and published skills compares a numeric aggregation alias, which is not judged. Callers of `engine.aggregate` and of the REST aggregate query in a deployment were NOT measured.
3838

39-
**Fix.** Compare a `date` column with a `YYYY-MM-DD` day, a `datetime` column with an ISO-8601 instant, a bare day or epoch milliseconds, and a `time` column with an `HH:MM` or `HH:MM:SS` wall clock.
39+
**Fix.** Compare a `date` column with a `YYYY-MM-DD` day, a `datetime` column with an ISO-8601 instant, a bare day or epoch milliseconds, either one with a relative-date placeholder the resolver knows (`{30_days_ago}`, `{current_month_start}`; `having` resolves them from the same release, #20334), and a `time` column with an `HH:MM` or `HH:MM:SS` wall clock.
4040

4141
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` and per-aggregation `filter` answer; every `having` on a temporal column whose comparand the rule reads (a `YYYY-MM-DD` day, an ISO instant, an epoch-millisecond number or string, an in-range `Date`, a zone-naive instant, a wall clock, an extended-year instant on a `datetime` column, which that rule reads); `{today}`-style placeholders, known or not; the empty and the whitespace-only string; `null`, `$exists`, `$in` / `$nin`, `$between`, `$not` / `$or` / `$and` and `{ $field }` references; `$contains` and `$startsWith`; every `count` / `sum` / `avg` column, a string comparand included; a `month` bucket; and every existing `having` refusal, in its words.

‎.changeset/20334-aggregate-positions.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ Measured before and after through `engine.aggregate` and `POST /data/:object/que
2020
| any known token, as a comparand, an `$in` member or a `$between` endpoint, under `$and` / `$or` / `$not` | compared as its own text (on a date or datetime column, `$gt` / `$gte` kept no group and `$lt` / `$lte` every group) | the groups the resolved value keeps, the same as that value written out | resolved |
2121
| `{ customer_id: '{current_user_id}' }` on a groupBy key, as user `c2` | keeps no group | keeps `c2` | resolved |
2222

23-
Tokens resolve the way they do in `where`: a date macro to a `YYYY-MM-DD` day (a sub-day macro to an ISO instant) in the request's timezone, `{current_user_id}` and `{current_org_id}` from the request. A string that only contains braces (`'a{b}c'`) is not a placeholder and compares as written, as in `where`. The `having` doors run first, as `where`'s do: a clause an earlier `having` door refuses (an unknown key, an unknown operator, a comparand its temporal column cannot read) keeps that refusal and its words.
23+
Tokens resolve the way they do in `where`: a date macro to a `YYYY-MM-DD` day (a sub-day macro to an ISO instant) in the request's timezone, `{current_user_id}` and `{current_org_id}` from the request. A string that only contains braces (`'a{b}c'`) is not a placeholder and compares as written, as in `where`. The `having` doors run first, as `where`'s do: a clause an earlier `having` door refuses (an unknown key, an unknown operator, a comparand its temporal column cannot read) keeps that refusal, in that door's words.
2424

2525
**The per-aggregation `filter`'s refusals name their position.** A comparand a declared temporal field cannot read, and a text operator aimed at a field that never holds a string, in `aggregations[1].filter` said `at where.placed_on.$gt` / `at where.amount.$contains`, a `where` the author did not write. They now say `at aggregations[1].filter.placed_on.$gt` / `at aggregations[1].filter.amount.$contains`, as that filter's list-shape and comparand-type refusals already did. The code (`INVALID_FILTER`), the status (400) and every other word are unchanged at the engine. Over REST, the message keeps its existing 500-character bound: a `date` field's refusal of a string such as `'not-a-date'`, which fitted within it, now loses the end of its remedy (`"{current_month_s…` at the shortest path), and the refusals that already exceeded the bound still do.
2626

27+
**The `having` temporal refusal's remedy is `where`'s.** A string a `date` or `datetime` aggregated column cannot read (`{ last_placed: { $lt: 'last_30_days' } }` on `max(placed_on)`) was refused with a remedy that named the literal forms only (`Write a "YYYY-MM-DD" calendar day.` on a `date` column), written when `having` resolved no placeholder. Now that it resolves them, the refusal ends in the remedy the same comparand gets in `where`, which names the placeholder: `Write a "YYYY-MM-DD" calendar day, or a relative-date placeholder the resolver knows, e.g. "{30_days_ago}" / "{current_month_start}".` on a `date` column, and the `where` remedy for a `datetime` field on a `datetime` column. The code (`INVALID_FILTER`), the status (400), what is refused and every word before the remedy are unchanged, and so are a `time` column's refusal and the refusal of a number or `Date` whose year falls outside 0000 to 9999. Over REST the message keeps its 500-character bound, which the longer remedy now reaches, measured with an object named `ledger_having`. A `date` column's refusal still arrives whole in every cell measured, the longest at 498 characters (`'+010000-01-01T00:00:00.000Z'` on `max(placed_on)`), and it stays whole while the object name, the column, what it aggregates, the comparand as quoted and its path take at most 90 characters together. A `datetime` column's refusal no longer fits whatever those are, because its fixed words and remedy alone take 502 characters: over REST it now ends inside the remedy, before the placeholder it names (`…epoch milliseconds, or a relative-date pl…` on `min(opened_at)`), as the `where` refusal for a `datetime` field already did. A preset name such as `'last_30_days'` does not reach this refusal over REST: the query schema refuses it first (`VALIDATION_FAILED`), before and after.
28+
2729
**Who is affected.** `having` is a request-only key (`QuerySchema.having`, `EngineAggregateOptions.having`), and no metadata type stores it. The five `having` clauses in this repository's docs and published skills compare a numeric aggregation alias with a number, and none carries a placeholder; no runtime code or example app in this repository composes a `having`. Callers of `engine.aggregate` and of the REST aggregate query in a deployment were NOT measured.
2830

2931
**Fix.** For an unknown token, write one the resolver knows (the refusal lists them: `{today}`, `{current_quarter_start}`, `{30_days_ago}`, `{current_user_id}`, …) or the literal value. For `{current_user_id}` / `{current_org_id}`, send the request with a user or an active organization.
3032

31-
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` answer, placeholders and refusals included; every `having` that carries no placeholder, its refusals in their words; every per-aggregation `filter` answer other than the two refusals' paths above, placeholders included.
33+
**Unchanged**, measured identical before and after on the three drivers, both paths and both doors: every `where` answer, placeholders and refusals included; every `having` that carries no placeholder, and its refusals in their words other than the `date` / `datetime` temporal refusal's remedy above; every per-aggregation `filter` answer other than the two refusals' paths above, placeholders included.

0 commit comments

Comments
 (0)