Skip to content

Commit ab75670

Browse files
committed
no-mistakes(review): Count NULL any/all conditions as failing; flag legacy excludes
1 parent 8bcf4b7 commit ab75670

19 files changed

Lines changed: 128 additions & 45 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ generator catalog and the Atlas evaluation are 1.2 work.
126126
or `all` may omit `@of` and declare a `@filter` or `@segment` instead: it is then true when
127127
some row (`any`) or every row (`all`) in the group passes that condition. `{ "name":
128128
"hasError", "@agg": "any", "@filter": { "success": false } }` states "did any call fail"
129-
(`bool_or(success = FALSE)`) over a `success` column, which a boolean `@of` could not. A row
130-
whose condition is NULL is skipped, as `bool_or` skips one. It lowers
131-
to `bool_or(<condition>)` / `bool_and(<condition>)` on Postgres and `MAX` / `MIN` of the
132-
condition on SQLite, D1 and MySQL, and to the same SQL in the Cube model. The registry makes
129+
over a `success` column, which a boolean `@of` could not. A row whose condition is NULL
130+
counts as not passing, as a `WHERE` drops it, so only a group with no rows reads NULL. It
131+
lowers to `bool_or(COALESCE(<condition>, FALSE))` / `bool_and(...)` on Postgres and `MAX` /
132+
`MIN` of `COALESCE(<condition>, 0)` on SQLite, D1 and MySQL, and to the same SQL in the Cube
133+
model. The registry makes
133134
`measure.aggregate` `@of` optional (`metamodelVersion` stays `1.1`), and a new loader rule M0
134135
refuses, with `ERR_INVALID_MEASURE`, an `@of`-less measure whose `@agg` is not `any` or `all`
135136
(it was `ERR_MISSING_REQUIRED_ATTR`) and an `@of`-less `any` / `all` with no condition.

‎agent-context/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

‎docs/features/cube-export.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ somewhere to go: an `identity.reference` onto an entity that is not a cube makes
322322
| `count` with `@distinct` | `type: count_distinct`, `sql: x` |
323323
| `count` with `@distinct` over a tuple `x1, x2` | Postgres: `type: count_distinct`, `sql: 'ROW(x1, x2)'`, with one `filters` entry `x1 IS NOT NULL AND x2 IS NOT NULL`, so a tuple with a null component is not counted, as in the view. MySQL: the view's own `COUNT(DISTINCT x1, x2)` as a `type: number` measure (a condition `c` makes it `COUNT(DISTINCT CASE WHEN c THEN x1 END, x2)`); MySQL's multi-argument form skips a tuple with a null component and compares by the columns' collation, which a `JSON_ARRAY(x1, x2)` key does not: on mysql:8.4 under the default `utf8mb4_0900_ai_ci` it counted `'abc'` and `'ABC'` as two tuples where the view counts one |
324324
| `sum`, `avg`, `min`, `max` | `type: sum`, `avg`, `min`, `max`, `sql: x` |
325-
| `any`, `all` | `type: boolean`. Postgres: `sql: 'bool_or(x)'` / `'bool_and(x)'`, a condition `c` as `bool_or(x) FILTER (WHERE c)`. MySQL: `sql: 'MAX(x)'` / `'MIN(x)'`, a condition as `MAX(CASE WHEN c THEN x END)`. The aggregate is the view's own, written in `sql` because Postgres has no `MAX(boolean)` and the condition cannot ride in `filters`. With no `@of`, the condition is the aggregated value: `bool_or(c)` / `bool_and(c)`, `MAX(c)` / `MIN(c)` on MySQL. |
325+
| `any`, `all` | `type: boolean`. Postgres: `sql: 'bool_or(x)'` / `'bool_and(x)'`, a condition `c` as `bool_or(x) FILTER (WHERE c)`. MySQL: `sql: 'MAX(x)'` / `'MIN(x)'`, a condition as `MAX(CASE WHEN c THEN x END)`. The aggregate is the view's own, written in `sql` because Postgres has no `MAX(boolean)` and the condition cannot ride in `filters`. With no `@of`, the condition is the aggregated value, a null condition counting as not passing: `bool_or(COALESCE(c, FALSE))` / `bool_and(COALESCE(c, FALSE))`, `MAX(COALESCE(c, 0))` / `MIN(COALESCE(c, 0))` on MySQL. |
326326
| any of the above with a condition `c` | the same, with one `filters` entry `c`. For a Postgres tuple, `c` is ANDed after the not-null terms in that one entry. |
327327
| `measure.ratio` | `type: number`. Postgres: `CAST({num} AS NUMERIC) / NULLIF({den}, 0)`. MySQL: `{num} / NULLIF({den}, 0)`. |
328328
| a `measure.aggregate` with `@default: n` | two members. `<m>Raw` is the aggregate as the rows above write it (its `type`, `sql` and `filters`), with `public: false`. `<m>` is `type: number` (`type: boolean` for an `any` or `all`, where `n` is written `TRUE` / `FALSE` on Postgres and `1` / `0` on MySQL), `sql: 'COALESCE({<m>Raw}, n)'`, and carries the measure's `title` and `description`. |

‎docs/features/prerelease.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,11 @@ per-worktree exclude), so the paths `link` adds there live inside their own mana
173173
too: `unlink` removes exactly that block, never a line a user added to the file by hand,
174174
and a repeated `link` does not duplicate it. An entry that another worktree of the same
175175
clone, or another project linked in the same worktree, still needs stays until that one is
176-
unlinked too. Files that *are* tracked by definition
176+
unlinked too. A clone linked by an earlier version of the script holds those entries as bare
177+
lines outside any block, which `unlink` cannot tell from a line a user added. `link` and
178+
`unlink` name such lines and print the one command that removes them, and leave them in
179+
place; `link` does not add a second copy inside the block while one is there. Run that
180+
command once no worktree of the clone is linked. Files that *are* tracked by definition
177181
(`pyproject.toml`, `pom.xml`) get a loud warning instead — there is no way to make an edit
178182
to a tracked file uncommittable, which is precisely why the detector in §5 exists.
179183

‎docs/features/reporting.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,9 +507,11 @@ A measure with no `@of` must declare a `@filter` or a `@segment` (rule M0); ever
507507
- **How it lowers.** Postgres writes `bool_or(x)` / `bool_and(x)`, with `FILTER (WHERE …)`
508508
for a condition. SQLite and MySQL have no boolean aggregate, and a boolean there is `0` or
509509
`1`, so they write `MAX(x)` for `any` and `MIN(x)` for `all` (`MAX(CASE WHEN … THEN x END)`
510-
for a condition). Without `@of` the condition is the aggregated value: `bool_or(<condition>)`
511-
on Postgres and `MAX(<condition>)` elsewhere, where a comparison is `0`, `1` or null. A row
512-
whose condition is null (a null `success`) is skipped, as a null `@of` is. The runtime reads the column as the boolean it is typed as. A default is
510+
for a condition). Without `@of` the condition is the aggregated value:
511+
`bool_or(COALESCE(<condition>, FALSE))` on Postgres and `MAX(COALESCE(<condition>, 0))`
512+
elsewhere, where a comparison is `0`, `1` or null. A row whose condition is null (a null
513+
`success`) counts as not passing, as a `WHERE` drops it, so `all` over `true, null` is `false`
514+
and only a group with no rows reads null. The runtime reads the column as the boolean it is typed as. A default is
513515
`COALESCE(<the aggregate>, FALSE)` on Postgres and `COALESCE(<the aggregate>, 0)` elsewhere.
514516
- **Cube.** The export writes a `type: boolean` measure holding the same aggregate, see
515517
[cube-export.md](cube-export.md).

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

‎fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

‎fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and an `object.report` combines them by name. Four node kinds:
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
1717
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
1818
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19-
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false";
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
2021
- `segment.filter` — a named, reusable `@filter` ("active purchase");
2122
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2223
`name:grain`), `@measures`.

0 commit comments

Comments
 (0)