Skip to content

Commit 99786f9

Browse files
fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer says to drop the key (#20709)
Fixes #20662 Clause-②: no (author-shown wording only; no accept or reject moves) ## What changes The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` leaves a record-form filter with a `null`-valued key as stored and reports it as a TODO, which `os migrate meta --stored` prints. Its reason said the renderer skips that key, so it "constrains nothing today", and told the operator to "Drop the key". At the `.objectui-sha` pin `dd3f7e1be356` that is true only on a block that queries an object. On a block whose rows are inline, the key selects the rows whose value is null, so dropping it widens the block. Following triage's direction (comment 5893989151: one wording true on both kinds of block, no "drop the key" advice), the reason now: - states what the key does on each kind of block: skipped where the block queries an object, so it constrains nothing; matched where its rows are inline (`data: { provider: 'value' }` or `staticData`), so it selects the rows whose value is null; - says that no one rule keeps both; - names the rule for the rows with no value, `{"field":"owner_id","operator":"is_null"}` (built from the key), and says that a filter which leaves the key unconstrained has no rule for it. It advises neither rewrite. The choice is the author's. The verdict does not move. The filter is still declined, left byte-identical and reported as one TODO, on any block. The reason text does not branch on where the rows come from; the conversion never reads that. Round 2 (seat note 5897754447, a claim amendment): the empty-operator-object reason beside it (`{ amount: {} }`) said the object "constrains nothing". At the pin the renderer refuses it instead. Where the block queries an object, `convertFiltersToAST` throws through `refuseEmptyOperatorMap` (`INVALID_FILTER`, 400). Where the block's rows are inline, `ValueDataSource.find` answers no rows through `zeroKeyConditionRefusal`. The reason and its docblock sentence now say that, say that no rule spells an operator object with no operator, and keep the renderer's own remedy, dropping the key. The verdict does not move. Files: - `packages/spec/src/conversions/registry.ts`: the reason string in `recordFilterToRules`, the sentence in its docblock, and the conversion entry docblock's parenthetical in "What is left exactly as stored" ("the renderer skips that key today"). That parenthetical is a fourth copy of the same claim in the same file. It is text only and fixed in place: same defect, same file under this claim, same gates. Round 2 rewrites the empty-operator-object declined reason and its docblock sentence, text only. Round 3 (review 5898951990) drops "a `data` array" from the null-key reason's inline list: at the pin a bare `data` array reaches no `ValueDataSource.find`. Round 4 makes the rationale in the conversion entry docblock's `## Reach` paragraph name the inline sources the filter reaches at the pin (`data: { provider: 'value' }` or `staticData`), and adds that a bare `data` array reaches none of them (`object-calendar` draws it unfiltered; `object-map` / `object-gantt` do not take it as a record source). Comment text only; the verdict sentence is unchanged. - `packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts`: the same claim in the D3 entry's `reason`. `packages/spec/src/migrations/registry.ts` is regenerated by `gen:migration-registry` and not edited by hand. Round 3 narrows the inline list in its older sentence ("None of this depends on where a block's rows come from …") to `data: { provider: 'value' }` or `staticData`. - `packages/spec/src/conversions/page-component-filter-record-to-rule-array.test.ts`: the `DECLINED_ROWS` comment that restated the null-key claim; the all-or-nothing test's comment, which named `owner_id: null` for a row that is `deleted_at: { $null: true }`; and two new pins. A null-valued key gets the same reason on an inline-row `object-map` and an object-bound one. That reason names the `is_null` rule for the key, and the block's door takes that rule; the control is that the door refuses the stored record. An empty operator object gets the same reason on both blocks, and that reason names `INVALID_FILTER`, a code in `StandardErrorCode`. - `.changeset/20662-null-key-todo-reason.md`: `@objectstack/spec` patch, covering both reasons. ## Verification record **Pin reading** (`dd3f7e1be356`, raw source): - `packages/core/src/utils/filter-converter.ts` `convertFiltersToAST` skips a key whose value is `null`/`undefined` (`skippedNullKeys`). - `packages/core/src/adapters/ValueDataSource.ts` `find` sends an object `$filter` to `matchesFilter`, and its simple-equality arm compares with `comparandEquals`, which is `value === target`. Its `is_null` arm is `value === null || value === undefined`. Server-side, `parseFilterAST` lowers `is_null` to `{ $null: true }`. - The inline branches of `ObjectMap` / `ObjectCalendar` pass `useResolvedFilter(schema.filter)` to `new ValueDataSource(...).find`. `filter-tokens.ts` `resolveContextTokens` returns a `null` value unchanged. - The #20305 dev's live probe (report 5893209491) measured that `find` with `{ owner_id: null }` selects only the null row, and not the `'u1'` row or the row that has no key. **Lit, at base `31ed067639`** (the stored-migration pass and `formatStoredMigrationReport`, the function `os migrate meta --stored` prints through, over a one-page stub `sys_metadata` holding an `object-grid` that queries `deal` and an `object-map` with `data: { provider: 'value' }`, both `filter: { owner_id: null }`): ```text TODO page-component-filter-record-to-rule-array: {"owner_id":null} left as stored at pages[0].regions[0].components[1].properties.filter — On the `object-map` block `inline`, this filter has the key `owner_id` set to null: the renderer skips a null-valued key, so today it constrains nothing, while an `equals` rule would test for null. Drop the key, or write a rule that tests for null if that is what it should select. Left as stored, it keeps loading unchanged, but it is not the rule-array form its door declares — rewrite it by hand. ``` The `object-grid` line carried the same sentence. **Dark, at `a51c02fe83`** (same probe, spec rebuilt): ```text TODO page-component-filter-record-to-rule-array: {"owner_id":null} left as stored at pages[0].regions[0].components[1].properties.filter — On the `object-map` block `inline`, this filter has the key `owner_id` set to null, and what that key selects depends on where the block's rows come from, so no one rule keeps it: where the block queries an object, the renderer skips a null-valued key, so it constrains nothing; where its rows are inline (`data: { provider: 'value' }`, a `data` array or `staticData`), it selects the rows whose `owner_id` is null. Decide which rows it should select: the rows with no `owner_id` value are the rule `{"field":"owner_id","operator":"is_null"}`, and a filter that leaves `owner_id` unconstrained has no rule for it. Left as stored, it keeps loading unchanged, but it is not the rule-array form its door declares — rewrite it by hand. ``` In both runs the row is still `skipped` with two TODOs. The probe file was temporary and is not in the diff. **Round 2, empty operator object** (same printer probe, with `filter: { amount: {} }` on the same two blocks). Lit at `c5eed1b4d3`: "... this filter has the key `amount` set to an empty operator object, which constrains nothing — and no rule says "nothing". Drop the key. Left as stored, ...". Dark at `3fcedfb564`: "... set to an empty operator object, which names the field and no operator, so no rule spells it. The renderer does not ignore it today: where the block queries an object, it refuses the filter (`INVALID_FILTER`, 400); where its rows are inline, it answers no rows. Drop the key. Left as stored, ...". Both runs: row `skipped`, two TODOs. Pin reading at `dd3f7e1be356`: `filter-converter.ts:818` calls `refuseEmptyOperatorMap`, whose `FilterOperatorError` has `code = 'INVALID_FILTER'` and `httpStatus = 400`; `ValueDataSource.find` answers `[]` when `zeroKeyConditionRefusal` returns a refusal. **Round 3, the inline list** (the null-key printer probe, rows `null` / `u1` / missing). Lit at `3fcedfb564`: "... where its rows are inline (`data: { provider: 'value' }`, a `data` array or `staticData`), it selects the rows whose `owner_id` is null. ...". Dark at `a6e54de377`: "... where its rows are inline (`data: { provider: 'value' }` or `staticData`), it selects the rows whose `owner_id` is null. ...". Pin reading at `dd3f7e1be356`: `record-source.ts:303-307` folds `staticData` to `{ provider: 'value', items }`, and the value branches hand the resolved filter to `ValueDataSource.find` (`ObjectMap.tsx:950-952`, `ObjectCalendar.tsx:708-710`, `ObjectTree.tsx:911-913`, `ObjectGantt.tsx:1009-1010`). A bare `data` array reaches none of them: `ObjectCalendar.tsx:387-389, 599-600, 647` draws it with no fetch and no filter, and the `view-data` arm refuses an array (`record-source.ts:179`). The round-1 Dark quote above is the `a51c02fe83` reading, before this narrowing. **Tests and gates, at `6f1396efa2`** (this branch merged with `origin/main` `671d4c164f` through `scripts/pm/os-regen-merge.sh`; the delta against main is exactly the five files above). Round 4's one-comment commit `eaf2d6e6e8` re-ran the conversions and migrations set (1034 passed), spec typecheck, `check:generated` (15 up to date), `check:doc-authoring` and `check:issue-citations`, all green; the derived gate list is unchanged: - `@objectstack/spec` `local` project: 575 files, 16972 passed, 1 todo. `typecheck`, including the test layer, passed. - `repo` project, narrowed to the three files that read the conversion and migration registries (`conversions-major18-merge`, `step18-rationale-merge`, `retired-key-migrate-sentence`): 35 passed. The full `repo` project did not finish inside the foreground cap and is NOT MEASURED locally; CI runs it. - `check:generated`: all 15 artifacts are up to date against a spec rebuilt after the merge. - `dispatch-gates --commands` derived 87 commands. 84 exited 0, including `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:docs`, `check:api-surface`, `check:authorable-surface`, `check:objectui-pin-citations`, `check:doc-authoring`, `check:nul-bytes` and `check:adr-0087-registration`. Three exited 3 with PREREQUISITE NOT MET, because they need a whole-workspace build: `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt`. They are NOT MEASURED locally. `--ran` reconciles 87 derived: 84 run, 3 NOT-MEASURED, 0 unrun. - Ablation of the new pin (`scripts/ablation-replace.mjs`, from the committed state): the anchor `operator: isNull })}` was replaced so the reason named an `equals`/`null` rule instead. The new test went red: expected the reason to contain `{"field":"owner_id","operator":"is_null"}`. The other five null-related tests stayed green. The file was restored: its blob matches HEAD and `git diff HEAD` is empty. - Round-2 ablation of the empty-operator pin, from committed `bcda701b88`: the anchor "block queries an object, it refuses the filter (`INVALID_FILTER`, 400); where its rows " was replaced with "block queries an object, it constrains nothing; where its rows ". The pin went red: expected the reason to contain `INVALID_FILTER`. The file was restored: its blob matches HEAD `f1f29e6f4802` and `git diff HEAD` is empty. ## Acceptance notes - No test pinned the false clause. The existing rows assert only the prefix "has the key `owner_id` set to null", so there was nothing to re-pin. The new pin checks named subjects: the same reason on both blocks, the `is_null` rule, and that the door takes it. It does not pin prose. The empty-operator reason was the same: only its prefix was asserted. - `packages/spec/CHANGELOG.md`'s 17.5.0 entry carries the old null-key sentence. It is a released record and is not edited; the corrected text ships in this PR's changeset. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f05919b commit 99786f9

5 files changed

Lines changed: 112 additions & 21 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer tells the operator to drop the key
6+
7+
The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` leaves a record-form filter with a `null`-valued key as stored and reports it as a TODO, which `os migrate meta --stored` lists. The TODO's reason used to say the renderer skips that key, so it "constrains nothing", and to "Drop the key". That holds only where the block queries an object. Where the block's rows are inline (`data: { provider: 'value' }` or `staticData`), the objectui version this repository pins matches the key against the rows and selects the rows whose value is null, so following the advice there widened what the block shows.
8+
9+
The reason now states both behaviours, says no one rule keeps both, and leaves the choice to the operator. For a stored `{ owner_id: null }` it names the rule `{"field":"owner_id","operator":"is_null"}` for the rows with no `owner_id` value, and says that a filter leaving `owner_id` unconstrained has no rule for it. The protocol-18 migration entry `element-data-source-and-object-block-filter-rule-array` says the same.
10+
11+
The TODO for a key set to an empty operator object (`{ amount: {} }`) also said it "constrains nothing". The renderer refuses it instead: where the block queries an object it refuses the filter with `INVALID_FILTER` (400), and where the block's rows are inline it shows no rows. The reason now says that, and keeps its advice to drop the key, which is the renderer's own remedy.
12+
13+
Nothing else changes. Both filters are still left exactly as stored and still reported as a TODO, on any block. No schema, conversion verdict or exit code moves.
14+
15+
Clause-②: no

‎packages/spec/src/conversions/page-component-filter-record-to-rule-array.test.ts‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import { describe, expect, it } from 'vitest';
3434

35+
import { StandardErrorCode } from '../api/errors.zod.js';
3536
import {
3637
FILTER_OPERATORS,
3738
VALID_AST_OPERATORS,
@@ -320,7 +321,8 @@ describe('§2 what has no lossless rule spelling is left byte-identical', () =>
320321
// The third column is what the site's TODO must say, or `null` for the one
321322
// row that is not a legacy form at all (see the last row).
322323
const DECLINED_ROWS: ReadonlyArray<readonly [string, unknown, string | null]> = [
323-
// The renderer at the pin skips a null key (constrains nothing); a rule would test IS NULL.
324+
// At the pin a null key constrains nothing where the block queries an object and selects
325+
// the null rows where its rows are inline — no one rule keeps both.
324326
['a null value', { owner_id: null }, 'has the key `owner_id` set to null'],
325327
['a null value beside a mappable key', { stage: 'open', owner_id: null }, 'has the key `owner_id` set to null'],
326328
// Direction lives in the VALUE — not in the one operator table.
@@ -357,7 +359,7 @@ describe('§2 what has no lossless rule spelling is left byte-identical', () =>
357359
});
358360

359361
it('all-or-nothing: a declined key keeps the mappable keys beside it from converting', () => {
360-
// Converting `stage` alone would drop `owner_id: null` from an AND list — a wider filter.
362+
// Converting `stage` alone would drop `deleted_at: { $null: true }` from an AND list — a wider filter.
361363
const { value } = gridFilter({ stage: 'open', deleted_at: { $null: true } });
362364
expect(value).toEqual({ stage: 'open', deleted_at: { $null: true } });
363365
});
@@ -646,6 +648,51 @@ describe('§8 the TODO channel — every site left as stored is reported (ruling
646648
expect(inline.todos[0]!.reason).toBe(bound.todos[0]!.reason);
647649
});
648650

651+
it('a null-valued key is a TODO in the same words on an inline-row node and an object-bound one, naming an `is_null` rule its door takes', () => {
652+
// At the objectui pin the key constrains nothing where the block queries an
653+
// object and selects the rows whose value is null where its rows are inline,
654+
// so no one rule keeps both: the one reason has to be true on either block.
655+
const inline = convert(
656+
pageWith({ type: 'object-map', properties: { staticData: [], filter: { owner_id: null } } }),
657+
);
658+
const bound = convert(
659+
pageWith({ type: 'object-map', properties: { objectName: 'deal', filter: { owner_id: null } } }),
660+
);
661+
expect((componentOf(inline.stack).properties as Dict).filter).toEqual({ owner_id: null });
662+
expect(inline.todos).toHaveLength(1);
663+
expect(inline.todos[0]!.reason).toBe(bound.todos[0]!.reason);
664+
const rule = { field: 'owner_id', operator: 'is_null' };
665+
expect(inline.todos[0]!.reason).toContain(JSON.stringify(rule));
666+
// The rule it names is one the block's door takes.
667+
const door = ComponentPropsMap['object-map'] as unknown as {
668+
safeParse: (v: unknown) => { error?: { issues: Array<{ path: PropertyKey[] }> } };
669+
};
670+
const atFilter = (v: unknown): number =>
671+
door.safeParse(v).error?.issues.filter((i) => i.path[0] === 'filter').length ?? 0;
672+
expect(atFilter({ objectName: 'deal', filter: [rule] })).toBe(0);
673+
// Control: the door really judges this key — the stored record is refused there.
674+
expect(atFilter({ objectName: 'deal', filter: { owner_id: null } })).toBeGreaterThan(0);
675+
});
676+
677+
it('an empty operator object is a TODO in the same words on an inline-row node and an object-bound one, naming the refusal the renderer answers', () => {
678+
// At the objectui pin the renderer refuses `{ amount: {} }` rather than
679+
// ignoring it — `INVALID_FILTER` where the block queries an object, no rows
680+
// where its rows are inline — and the one reason says so on either block.
681+
const inline = convert(
682+
pageWith({ type: 'object-map', properties: { staticData: [], filter: { amount: {} } } }),
683+
);
684+
const bound = convert(
685+
pageWith({ type: 'object-map', properties: { objectName: 'deal', filter: { amount: {} } } }),
686+
);
687+
expect((componentOf(inline.stack).properties as Dict).filter).toEqual({ amount: {} });
688+
expect(inline.todos).toHaveLength(1);
689+
expect(inline.todos[0]!.reason).toBe(bound.todos[0]!.reason);
690+
const code = 'INVALID_FILTER';
691+
// The code it names is one the platform declares.
692+
expect(StandardErrorCode.options).toContain(code);
693+
expect(inline.todos[0]!.reason).toContain(`\`${code}\``);
694+
});
695+
649696
it('names the block by its type, and by its `id` when it has one', () => {
650697
const { todos } = convert(
651698
pageWith({ type: 'object-kanban', id: 'pipeline_board', properties: { objectName: 'deal', filter: { $or: [] } } }),

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11314,11 +11314,22 @@ function dollarKeysReason(keys: readonly string[]): string {
1131411314
* — `$and` / `$or` / `$not` above all — is not a field, so its record is left
1131511315
* alone; that is the ruled boundary, and flattening a combinator into the AND
1131611316
* list is exactly the silent selection change it excludes. A `null` value is
11317-
* declined too, and not for a schema reason: the renderer at the
11318-
* `.objectui-sha` pin (`convertFiltersToAST`) SKIPS a record key whose value is
11319-
* null, so that key constrains nothing today, while an `equals null` rule would
11320-
* test IS NULL. An empty operator object is declined for the same reason — it
11321-
* constrains nothing, and no rule says "nothing".
11317+
* declined too, and not for a schema reason: at the `.objectui-sha` pin the key
11318+
* selects different rows on different blocks, so no one rule keeps it. Where a
11319+
* block queries an object, `convertFiltersToAST` SKIPS a record key whose value
11320+
* is null, so the key constrains nothing; where a block's rows are inline,
11321+
* `ValueDataSource.find` matches the record through `comparandEquals`, so the
11322+
* key selects the rows whose value is null. This entry never reads where a
11323+
* block's rows come from, so its reason states both and advises neither
11324+
* rewrite: it names the `is_null` rule for the rows with no value, and leaves
11325+
* which rows the filter should select to the author. An empty operator object
11326+
* is declined for a different reason: it names a field and no operator, so no
11327+
* rule spells it. At the same pin the renderer refuses it rather than ignoring
11328+
* it — where a block queries an object, `convertFiltersToAST` throws through
11329+
* `refuseEmptyOperatorMap` (`INVALID_FILTER`, 400); where a block's rows are
11330+
* inline, `ValueDataSource.find` answers no rows through
11331+
* `zeroKeyConditionRefusal`. Its reason says both and keeps the renderer's own
11332+
* remedy, dropping the key.
1132211333
*
1132311334
* Every top-level `$` key is judged before any field key, so the reason names
1132411335
* the combinator even when a field key beside it would decline as well. The
@@ -11336,10 +11347,15 @@ function recordFilterToRules(record: Record<string, unknown>): FilterMapping {
1133611347
continue;
1133711348
}
1133811349
if (value === null) {
11350+
const isNull = 'is_null' satisfies ViewFilterOperator;
1133911351
return {
11340-
declined: `has the key \`${field}\` set to null: the renderer skips a null-valued key, so `
11341-
+ `today it constrains nothing, while an \`${equals}\` rule would test for null. Drop the `
11342-
+ 'key, or write a rule that tests for null if that is what it should select',
11352+
declined: `has the key \`${field}\` set to null, and what that key selects depends on where `
11353+
+ 'the block\'s rows come from, so no one rule keeps it: where the block queries an object, '
11354+
+ 'the renderer skips a null-valued key, so it constrains nothing; where its rows are inline '
11355+
+ '(`data: { provider: \'value\' }` or `staticData`), it selects the rows whose '
11356+
+ `\`${field}\` is null. Decide which rows it should select: the rows with no \`${field}\` `
11357+
+ `value are the rule \`${JSON.stringify({ field, operator: isNull })}\`, and a filter that `
11358+
+ `leaves \`${field}\` unconstrained has no rule for it`,
1134311359
};
1134411360
}
1134511361
if (!isRecordForm(value)) {
@@ -11352,8 +11368,10 @@ function recordFilterToRules(record: Record<string, unknown>): FilterMapping {
1135211368
const operators = Object.entries(value);
1135311369
if (operators.length === 0) {
1135411370
return {
11355-
declined: `has the key \`${field}\` set to an empty operator object, which constrains `
11356-
+ 'nothing — and no rule says "nothing". Drop the key',
11371+
declined: `has the key \`${field}\` set to an empty operator object, which names the field `
11372+
+ 'and no operator, so no rule spells it. The renderer does not ignore it today: where the '
11373+
+ 'block queries an object, it refuses the filter (`INVALID_FILTER`, 400); where its rows '
11374+
+ 'are inline, it answers no rows. Drop the key',
1135711375
};
1135811376
}
1135911377
for (const [op, comparand] of operators) {
@@ -11525,7 +11543,8 @@ function describeBlock(component: Dict): string {
1152511543
*
1152611544
* A record carrying `$and` / `$or` / `$not` (or any top-level `$` key), an AST
1152711545
* `and` / `or` group, an operator the rule vocabulary does not spell (`$null`,
11528-
* `$exists`, `like`, …), a `null` value (the renderer skips that key today),
11546+
* `$exists`, `like`, …), a `null` value (skipped where a block queries an
11547+
* object, matched where its rows are inline — no one rule keeps both),
1152911548
* an array or object comparand in equality position, and any rule the door
1153011549
* would refuse. All-or-nothing per filter: converting part of an AND-list
1153111550
* widens it. ⛔ A combinator is never flattened into the AND list — for `$or`
@@ -11566,10 +11585,14 @@ function describeBlock(component: Dict): string {
1156611585
* `staticData`) is rewritten exactly as a block that queries an object:
1156711586
* measured at the `.objectui-sha` pin `dd3f7e1be356`, the renderers that match
1156811587
* inline rows in memory (`object-map`, `object-tree`, `object-calendar`,
11569-
* `object-gantt`, through `ValueDataSource.find`) lower a rule array through
11588+
* `object-gantt`, through `ValueDataSource.find`) take those rows from
11589+
* `data: { provider: 'value' }` or `staticData`, lower a rule array through
1157011590
* the grid's own sink before matching, and select the same rows for it as for
1157111591
* the stored form — every mapped operator, against a control where the
11572-
* lowering is absent and the rule array selects none.
11592+
* lowering is absent and the rule array selects none. A bare `data` array
11593+
* reaches none of them: `object-calendar` draws it as pre-fetched rows with no
11594+
* filter applied, and `object-map` / `object-gantt` do not take it as a record
11595+
* source.
1157311596
*
1157411597
* ## Why `retiredFromLoadPath`
1157511598
*

‎packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ export const entry: SemanticMigration = {
7070
+ 'path: an author writing the record form is still refused at the `filter` door. ⚠️ A '
7171
+ 'filter carrying `$and` / `$or` / `$not` is left exactly as stored — the rule array '
7272
+ 'only ANDs, and flattening a combinator changes which rows the page selects — and so is '
73-
+ 'any filter with a part that has no lossless rule spelling: a `null` value (the renderer '
74-
+ 'skips that key, so it constrains nothing today, where a rule would test IS NULL), an '
73+
+ 'any filter with a part that has no lossless rule spelling: a `null` value (where a block '
74+
+ 'queries an object the renderer skips that key, so it constrains nothing, and where its '
75+
+ 'rows are inline it selects the rows whose value is null — no one rule keeps both, so the '
76+
+ 'TODO names the `is_null` rule for the rows with no value and leaves which rows to select '
77+
+ 'to the author), an '
7578
+ 'operator such as `$null` / `$exists` or an AST `like`, an array or object comparand in '
7679
+ 'equality position, or an AST `and` / `or` group. None of this depends on where a '
7780
+ 'block\'s rows come from: a filter on a component whose rows are inline (`data: { '
78-
+ 'provider: \'value\' }`, a `data` array, or `staticData`) — the binding\'s included — is '
81+
+ 'provider: \'value\' }` or `staticData`) — the binding\'s included — is '
7982
+ 'rewritten or left exactly as it would be on a block that queries an object, because the '
8083
+ '`object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks of the objectui '
8184
+ 'version this release pins match a rule array against those rows and select the rows the '

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9397,12 +9397,15 @@ const step18: MigrationStep = {
93979397
+ 'path: an author writing the record form is still refused at the `filter` door. ⚠️ A '
93989398
+ 'filter carrying `$and` / `$or` / `$not` is left exactly as stored — the rule array '
93999399
+ 'only ANDs, and flattening a combinator changes which rows the page selects — and so is '
9400-
+ 'any filter with a part that has no lossless rule spelling: a `null` value (the renderer '
9401-
+ 'skips that key, so it constrains nothing today, where a rule would test IS NULL), an '
9400+
+ 'any filter with a part that has no lossless rule spelling: a `null` value (where a block '
9401+
+ 'queries an object the renderer skips that key, so it constrains nothing, and where its '
9402+
+ 'rows are inline it selects the rows whose value is null — no one rule keeps both, so the '
9403+
+ 'TODO names the `is_null` rule for the rows with no value and leaves which rows to select '
9404+
+ 'to the author), an '
94029405
+ 'operator such as `$null` / `$exists` or an AST `like`, an array or object comparand in '
94039406
+ 'equality position, or an AST `and` / `or` group. None of this depends on where a '
94049407
+ 'block\'s rows come from: a filter on a component whose rows are inline (`data: { '
9405-
+ 'provider: \'value\' }`, a `data` array, or `staticData`) — the binding\'s included — is '
9408+
+ 'provider: \'value\' }` or `staticData`) — the binding\'s included — is '
94069409
+ 'rewritten or left exactly as it would be on a block that queries an object, because the '
94079410
+ '`object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks of the objectui '
94089411
+ 'version this release pins match a rule array against those rows and select the rows the '

0 commit comments

Comments
 (0)