Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/plugin-detail-8937-parent-scope-residue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@object-ui/plugin-detail': patch
'@object-ui/core': patch
---

docs(parent-scope): state the driver's real arity rule, and the arity-dependent parent scope in the shipped README (objectui#8937)

Two published texts that objectui#8886 left behind, both measured false on `origin/main`:

- **`packages/plugin-detail/README.md` (it is in `files[]`, so it ships).** It said the
node's `filter` is AND-combined with `{ [relationshipField]: parentId }`, full stop.
Since objectui#7299 the parent condition is compiled to match the relationship field's
arity, so a multi-valued relationship gets
`{ [relationshipField]: { $contains: parentId } }` instead. The paragraph now states
both spellings and names the arbiter (`@objectstack/spec/data`'s `isMultiValueField`).
- **The claim that the SQL driver decides arity on that same predicate.** It does not:
`driver-sql` gates the equality family on its own storage question, which reads
`multiple` as truthy on ANY type. The two rules therefore disagree for a type outside
`MULTI_CAPABLE_TYPES` carrying `multiple: true`. objectui#9184 moved the arity compiler
into `@object-ui/core`'s `parent-scope` seam and carried the claim with it, so the
correction is recorded there — the seam now states the driver's measured rule, records
the divergence as a divergence, and points at the upstream card that owns which of the
two rules is right (objectstack#17469). `RelatedList.tsx`'s pointer comment and the
objectui#7299 test header carried the same sentence and are corrected to match.

No predicate moved and no wire changed — this release carries corrected published text
only. All three claims are pinned by re-derivation rather than transcription, in
`relatedListParentScopeResidue-8937.test.ts`.
40 changes: 35 additions & 5 deletions packages/core/src/utils/parent-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
*
* ## The rule itself is not invented here
*
* The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`, the
* same predicate the driver that executes the query decides on. That matters
* more here than anywhere: this function chooses `$contains` vs `=`, the driver
* chooses whether to accept it, and two readers of one question disagreeing is
* the entire defect class. The spec's rule is BROADER than an eyeballed
* The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`. That
* matters more here than anywhere: this function chooses `$contains` vs `=` and
* every surface that asks the question now reads that one choice, so two
* readers of one question disagreeing — the entire defect class — cannot
* recur INSIDE this repository. The spec's rule is BROADER than an eyeballed
* `multiple === true` in both directions — `multiselect` / `checkboxes` /
* `tags` persist an array with no flag at all, and `multiple: true` is INERT on
* a type outside the spec's multi-capable set (`master_detail`, say) — so a
Expand All @@ -36,6 +36,36 @@
* ⛔ Do not add a local arity rule at any call site, however small, and ⛔ do
* not widen this function to accept an arity the caller computed: the parameter
* it takes is METADATA, and the verdict is drawn from it here.
*
* ## ⚠️ The STORAGE side does not read this predicate — the two rules DIVERGE
*
* This header used to say `isMultiValueField` is "the same predicate the driver
* that executes the query decides on". It is not, and the difference is
* observable (objectui#8937). Measured on objectstack `origin/main`,
* `driver-sql` gates the equality family on its own STORAGE question,
* `isJsonField`: a column is JSON when the field's type is in that driver's
* `JSON_COLUMN_TYPES` — the spec's `STRUCTURED_JSON_TYPES` and
* `MULTI_OPTION_TYPES`, plus the driver-internal `object` / `array` aliases —
* OR when `multiple` is merely TRUTHY, on ANY type; a single-value media type
* answers from the ADR-0104 dual-encoding window instead. The spec's predicate
* is `MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) &&
* multiple === true)`.
*
* ⇒ They diverge for a type OUTSIDE `MULTI_CAPABLE_TYPES` carrying
* `multiple: true` (`master_detail` / `tree` / `text`): the spec says
* single-valued, so this seam compiles `=`, while the driver stores a JSON
* column and refuses `=` with the same `400 INVALID_FILTER` objectui#7299 was
* filed for. That is a KNOWN divergence and ⛔ not a regression — both
* surfaces sent `=` for that shape before objectui#7299 and objectui#8882 too.
*
* ⛔ Do NOT close it by widening the predicate here: which of the two rules is
* right is an upstream question, filed as objectstack#17469 (enforce-or-align),
* and this seam is deliberately not blocked on it. Widening on this side would
* move the disagreement across the wire rather than end it, and would make the
* ONE compiler this module exists to be disagree with the spec it delegates to.
* The divergence is re-derived from the installed spec on every run by
* `relatedListParentScopeResidue-8937.test.ts`, so this section reddens when the
* spec side moves.
*/

import { isMultiValueField, type ValueShapeFieldDef } from '@objectstack/spec/data';
Expand Down
11 changes: 9 additions & 2 deletions packages/plugin-detail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,16 @@ filter can express).

The node's `filter` (spec `RecordRelatedListProps.filter`, "additional filter
criteria") narrows the list beyond the parent relationship: it is
**AND-combined** with `{ [relationshipField]: parentId }`, never substituted for
**AND-combined** with the parent-relationship condition, never substituted for
it, so a related list stays scoped to the record it appears on and an additional
criterion can only ever narrow that set. Authors write it in the spec's own
criterion can only ever narrow that set. That condition is **not one fixed
spelling** — it is compiled to match the relationship field's ARITY on the
child object (`objectui#7299`). A single-valued relationship gets the equality
form `{ [relationshipField]: parentId }`; a multi-valued one gets the membership
form `{ [relationshipField]: { $contains: parentId } }`, because the stored
value is an array of ids and equality would ask whether the whole array *is* one
id. The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`,
never a rule this package keeps locally. Authors write `filter` in the spec's own
vocabulary (`[{ field, operator, value }]`); a `dataSource` binding's composed
filter (component AND saved view AND binding) lands on the same key. Both are
lowered to ObjectQL through the repo's single filter sink, so no second dialect
Expand Down
18 changes: 13 additions & 5 deletions packages/plugin-detail/src/RelatedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,19 @@ export const RelatedToolbarButton: React.FC<{
*
* ⛔ Do not reintroduce a local arity rule here, however small — the warning
* that stood at this spot still stands, and now names one more reader. This
* component decides `$contains` vs `=`, the driver that refuses the query
* decides on `@objectstack/spec/data`'s `isMultiValueField`, and the badge
* decides too; readers of one question disagreeing is the whole defect class.
* Moving the decision to a shared seam is NOT "putting a local rule one layer
* up" — the rule is still the spec's, and there is now exactly one caller of it.
* component decides `$contains` vs `=` and the badge decides too; readers of
* one question disagreeing is the whole defect class. Moving the decision to a
* shared seam is NOT "putting a local rule one layer up" — the rule is still
* the spec's, and there is now exactly one caller of it.
*
* ⚠️ What the seam does NOT buy is agreement with STORAGE. This spot used to
* add that the driver refusing the query decides on that same
* `isMultiValueField`. It does not (objectui#8937): `driver-sql` gates the
* equality family on its own storage question, which reads `multiple` as truthy
* on ANY type, so the two rules diverge for `master_detail` / `tree` / `text`
* carrying `multiple: true`. The measured rule, the divergence and the upstream
* card that owns which of them is right (objectstack#17469) are recorded on the
* seam itself — `@object-ui/core`'s `parent-scope` — so one place answers it.
*/

export const RelatedList: React.FC<RelatedListProps> = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,21 @@
*
* The arity VERDICT is `@objectstack/spec/data`'s exported `isMultiValueField`,
* imported rather than re-implemented — the component only finds the field def
* to hand it. That is not hygiene: this component picks `$contains` vs `=` on
* the answer, and the driver that refuses the query picks on the spec's, so a
* local copy would be two readers of one question disagreeing, which is the
* defect this card is about. The three `SPEC PREDICATE` cases below pin the
* places where the spec's rule and an eyeballed `multiple === true` DIFFER,
* measured on `@objectstack/spec` 17.4.0, in both directions.
* to hand it. That is not hygiene: a second copy of the rule inside this
* package would be two readers of one question disagreeing, which is the defect
* this card is about. The three `SPEC PREDICATE` cases below pin the places
* where the spec's rule and an eyeballed `multiple === true` DIFFER, measured
* on `@objectstack/spec` 17.4.0, in both directions.
*
* ⚠️ This header used to add that "the driver that refuses the query picks on
* the spec's". It does NOT (objectui#8937). `driver-sql` gates the equality
* family on its own STORAGE question — a JSON column when the type is one it
* stores as JSON OR when `multiple` is merely TRUTHY, on ANY type — so the two
* rules diverge for a type outside `MULTI_CAPABLE_TYPES` carrying
* `multiple: true`. See the `INERT` case below and `parentRelationshipFieldDef`'s
* docblock in `RelatedList.tsx`; the divergence itself is re-derived each run by
* `relatedListParentScopeResidue-8937.test.ts` and owned upstream by
* objectstack#17469.
*
* ## The control
*
Expand Down Expand Up @@ -308,12 +317,22 @@ describe('RelatedList — a related list on a MULTI-VALUE relationship returns d
});

it('SPEC PREDICATE — `multiple: true` is INERT on a type outside MULTI_CAPABLE_TYPES', async () => {
// The same delegation read the other way. `master_detail` is not a
// multi-capable type, so the flag does not make its stored value an array
// and equality is the correct predicate. An eyeballed `multiple === true`
// would send `$contains` against a scalar column — wrong in the opposite
// direction, and invisible, because it fails as "no rows" rather than as a
// refusal.
// The same delegation read the other way: `master_detail` is not a
// multi-capable type, so the SPEC's rule says the flag does not make the
// stored value an array, and this component sends `=`.
//
// ⚠️ What this case pins is the component's DELEGATION, ⛔ not that `=` is
// the predicate the storage will answer (objectui#8937). `driver-sql` reads
// `multiple` as truthy on ANY type, stores a JSON column, and refuses `=`
// here with the same `400 INVALID_FILTER` this card was filed for. That
// divergence is KNOWN and owned upstream (objectstack#17469); it is ⛔ not a
// regression of this card — the pre-fix renderer sent `=` for this shape
// too — and ⛔ not repairable by widening the predicate on this side, which
// would only move the disagreement rather than end it.
//
// The opposite error is still real and still guarded: an eyeballed
// `multiple === true` would send `$contains` against a genuinely scalar
// column, which fails as "no rows" rather than as a refusal.
const ds = makeDS(
recordSchema('contact', { account: { type: 'master_detail', reference: 'account', multiple: true } }),
SINGLE_ROWS,
Expand Down
Loading
Loading