diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 7bc962206..fe8346786 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1127,13 +1127,53 @@ When a field is defined as a [relationship](../database/schema.md#relationships) A to-many relationship resolves to an array of records; depending on access pattern you may need to `await` the property before iterating it. -**Join behavior:** selecting a relationship without an outer condition on it behaves as a **LEFT JOIN**: parents with no related row remain present, with the relationship omitted or empty. A top-level `conditions` entry on a related attribute (for example, `attribute: ['author', 'name']`) behaves as an **INNER JOIN**: it removes parents without a matching related row and restricts the selected related records. A `conditions` array inside a nested `select` object only filters the displayed children; it does not remove a parent. +**Join behavior:** selecting a relationship without an outer condition on it behaves as a **LEFT JOIN**: parents with no related row remain present, with the relationship omitted or empty. A top-level `conditions` entry on a related attribute (for example, `attribute: ['author', 'name']`) joined with `and` behaves as an **INNER JOIN**: it removes parents without a matching related row and restricts the selected related records. Inside an `or` group, the same entry only decides which parents match (see below). A `conditions` array inside a nested `select` object only filters the displayed children; it does not remove a parent. + +**AND conditions on one relationship match one related record.** Top-level conditions joined with `and`, including those in nested `and` groups, that traverse the same to-many relationship must all be met by the same related record. In the [sample brand and product data](../rest/querying.md#filtering-and-paging-each-relationship), Acme sells a Cable for 10, a Keyboard for 35, and a Dock for 60. This search finds no brand, because no Acme product is a Cable that costs at least 30: + +```javascript +const brands = []; +for await (const brand of Brand.search({ + conditions: [ + { attribute: 'name', value: 'Acme' }, + { attribute: ['products', 'name'], value: 'Cable' }, + { attribute: ['products', 'price'], comparator: 'greater_than_equal', value: 30 }, + ], + select: ['id', 'name'], +})) { + brands.push(brand); +} +``` + +With `'Keyboard'` in place of `'Cable'`, the search returns Acme, and selecting `products` returns only the Keyboard. A condition inside an `or` group is matched separately (see below). A collection `DELETE` follows the same rule; see the [REST example](../rest/querying.md#conditions-on-one-relationship-match-one-related-record). A to-one relationship has at most one related record, so the rule matters only for to-many relationships. Before v5.4.0, different related records could meet these conditions; in v5.3.5, this search returned Acme. + +**A relationship condition inside `or` decides parents only.** It does not restrict which related records `select` returns for a matching parent. The same applies to a top-level `operator: 'or'`: + +```javascript +const brands = []; +for await (const brand of Brand.search({ + conditions: [ + { + operator: 'or', + conditions: [ + { attribute: ['products', 'name'], value: 'Cable' }, + { attribute: 'id', value: 44 }, + ], + }, + ], + select: ['id', { name: 'products', select: ['name'] }], +})) { + brands.push(brand); +} +``` + +`brands` contains Acme with all three of its products and Studio Gear with Stand and Monitor. Without the `or` group, the Cable condition would restrict Acme's `products` to the Cable. An `and` condition beside the `or` group still restricts the selection, and a different related record can meet the `or` group's condition: adding `{ attribute: ['products', 'price'], comparator: 'greater_than_equal', value: 30 }` to the top-level `conditions` returns Acme with Keyboard and Dock (its Cable meets the `or` group) and Studio Gear with Stand and Monitor. Before v5.4.0, the `or` condition also restricted the selection; in v5.3.5, this search returned Acme with only its Cable and Studio Gear with an empty `products` array. #### Constraining related records :::note Pending release -Targeted for v5.4.0; not released yet. See [Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279) and [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775). +Targeted for v5.4.0; not released yet. See [Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279), [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775), and [Keep an AND group's estimate finite when a sibling condition is filter-only](https://github.com/HarperFast/harper/pull/3724). ::: @@ -1170,7 +1210,7 @@ The single-record example returns: } ``` -**Outer relationship conditions also restrict nested results.** You do not need to repeat an outer product-price condition inside the `products` selection. For example, this returns only brands with products costing at least 30, and includes their cheapest qualifying product: +**Outer relationship conditions joined with `and` also restrict nested results.** You do not need to repeat an outer product-price condition inside the `products` selection. For example, this returns only brands with products costing at least 30, and includes their cheapest qualifying product: ```javascript const brands = []; @@ -1217,7 +1257,7 @@ Add a nested `conditions` array only to narrow these inherited candidates furthe For production collections, also set a top-level `limit` to bound parents. Use a unique final sort key for both parent and child order, and remember that a small child limit does not bound sorting work. See [parent pagination and stable sorting](../rest/querying.md#bound-parent-pages-and-use-stable-sorting) for an example and performance guidance. -These options apply independently to each parent's children in both single-record `get()` and collection queries. `limit` is a count; `offset` is the number of eligible children to skip. These nested options do not remove parents: a parent with no matching children remains present with an empty to-many array. Nested conditions qualify immediate children; descendant filtering requires a selection on that descendant. Top-level `conditions` on that relationship still restrict the child candidates available to the nested selection, so an identical condition need not be repeated. +These options apply independently to each parent's children in both single-record `get()` and collection queries. `limit` is a count; `offset` is the number of eligible children to skip. These nested options do not remove parents: a parent with no matching children remains present with an empty to-many array. Nested conditions qualify immediate children; descendant filtering requires a selection on that descendant. Top-level `conditions` on that relationship joined with `and` still restrict the child candidates available to the nested selection, so an identical condition need not be repeated; one inside an `or` group does not. See [Filtering and Paging Each Relationship](../rest/querying.md#filtering-and-paging-each-relationship) for missing-value behavior, authorization, cache-local eligibility and ordering, scan costs, and unsupported constraints. @@ -1253,7 +1293,7 @@ Product.search({ Alternatively, pass `allowFullScan: true` to permit an unconditional ordered scan, or — if order doesn't matter — omit `sort` entirely and `search({})` will iterate without an index requirement. - When `attribute` is a path array that traverses a to-many relationship, each parent's sort key is the list of its related records' values in relationship order: the related records' primary-key order in the foreign-key index for a `@relationship(to: ...)` field (not sorted by the target attribute or reduced to a min/max), or the stored foreign-key array order for a `@relationship(from: ...)` array. Top-level `conditions` on a relationship restrict which parents match and which children `select` returns, but do not narrow the related records loaded into each parent's sort key. Lists compare element by element (`['alpha']` < `['alpha', 'delta']` < `['bravo']`), and `descending: true` reverses that order. A parent with no related records has an empty list (`[]`), a related record that omits the sort attribute (or an unresolved ID in a `@relationship(from: ...)` array without `filterMissing`) produces `undefined` at that position, and an explicit `null` attribute produces `null`: both `null` and `undefined` sort before booleans, numbers, and strings (`[]` and `[undefined]` compare as equal because reading past the end of a shorter array yields `undefined`). Across multiple to-many hops, each hop produces a list in relationship order, so two to-many hops compare a nested list of lists per parent (`[]` < `[[], ['a']]` < `[['b']]`). The index-or-condition rule above checks `@indexed` on the target attribute at the end of the path. List-per-parent sorting runs in memory when the target attribute is not `@indexed` (with a condition or `allowFullScan: true`), when a more selective condition on another attribute leads the query, or when `enforceExecutionOrder: true` is set, loading the related records for every matched parent; if the target attribute is `@indexed` and no more selective condition on another attribute leads the query, Harper instead drives the scan from the related table's index join (yielding each parent at its first matching index entry and, when unconditional, omitting parents with no related records). Note that, unlike a nested `select` sort (`select: [{ name: 'products', sort: { attribute: 'price' } }]`), which checks read permissions on the child sort attribute and orders children within each parent, a top-level `sort` orders the parent records and does not currently enforce `attribute_permissions` on the related sort attribute, so restricting read access to a child attribute does not prevent ordering parent records by it: + When `attribute` is a path array that traverses a to-many relationship, each parent's sort key is the list of its related records' values in relationship order: the related records' primary-key order in the foreign-key index for a `@relationship(to: ...)` field (not sorted by the target attribute or reduced to a min/max), or the stored foreign-key array order for a `@relationship(from: ...)` array. Top-level `conditions` on a relationship restrict which parents match and (when joined with `and`) which children `select` returns, but do not narrow the related records loaded into each parent's sort key. Lists compare element by element (`['alpha']` < `['alpha', 'delta']` < `['bravo']`), and `descending: true` reverses that order. A parent with no related records has an empty list (`[]`), a related record that omits the sort attribute (or an unresolved ID in a `@relationship(from: ...)` array without `filterMissing`) produces `undefined` at that position, and an explicit `null` attribute produces `null`: both `null` and `undefined` sort before booleans, numbers, and strings (`[]` and `[undefined]` compare as equal because reading past the end of a shorter array yields `undefined`). Across multiple to-many hops, each hop produces a list in relationship order, so two to-many hops compare a nested list of lists per parent (`[]` < `[[], ['a']]` < `[['b']]`). The index-or-condition rule above checks `@indexed` on the target attribute at the end of the path. List-per-parent sorting runs in memory when the target attribute is not `@indexed` (with a condition or `allowFullScan: true`), when a more selective condition on another attribute leads the query, or when `enforceExecutionOrder: true` is set, loading the related records for every matched parent; if the target attribute is `@indexed` and no more selective condition on another attribute leads the query, Harper instead drives the scan from the related table's index join (yielding each parent at its first matching index entry and, when unconditional, omitting parents with no related records). Note that, unlike a nested `select` sort (`select: [{ name: 'products', sort: { attribute: 'price' } }]`), which checks read permissions on the child sort attribute and orders children within each parent, a top-level `sort` orders the parent records and does not currently enforce `attribute_permissions` on the related sort attribute, so restricting read access to a child attribute does not prevent ordering parent records by it: ```javascript // On the Brand/Product sample in REST Querying, orders qualifying brands by diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 72faec949..a518aa452 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -309,19 +309,50 @@ GET /Product/?brand.name=Microsoft&select(name,brand{name}) GET /Product/?name=Keyboard&select(name,brand{name,id}) ``` -Selecting a relationship without an outer dot-syntax filter on it behaves as a LEFT JOIN: parents remain present even when there are no related records. For a singular foreign key that is null or references a missing record, the relationship property is omitted. An outer filter such as `products.price=ge=number:30` instead removes parents without a qualifying product and also narrows the selected products. A filter inside `select(...,products[...])` only narrows the projected products; it does not remove a parent. See the [worked scenarios](#filtering-and-paging-each-relationship) below. +Selecting a relationship without an outer dot-syntax filter on it behaves as a LEFT JOIN: parents remain present even when there are no related records. For a singular foreign key that is null or references a missing record, the relationship property is omitted. An outer filter joined with `&`, such as `products.price=ge=number:30`, instead removes parents without a qualifying product and also narrows the selected products. Inside an OR, an outer filter only [decides which parents match](#relationship-conditions-inside-an-or-decide-parents-only). A filter inside `select(...,products[...])` only narrows the projected products; it does not remove a parent. See the [worked scenarios](#filtering-and-paging-each-relationship) below. + +#### Conditions on one relationship match one related record + + + +Outer filters joined with `&`, including those in nested `&` groups, that traverse the same to-many relationship must all be met by one related record. In the [sample data](#filtering-and-paging-each-relationship) below, Acme sells a Cable for 10, a Keyboard for 35, and a Dock for 60: + +```http +GET /Brand/?name=Acme&products.name=Cable&products.price=ge=number:30 +GET /Brand/?name=Acme&products.name=Keyboard&products.price=ge=number:30&select(id,name,products{name,price}) +``` + +The first request returns `[]`: Acme has a Cable and has products costing at least 30, but no Cable costing at least 30. A collection `DELETE` with those conditions deletes nothing. The second request returns Acme with only its Keyboard. Filters that no single related record can meet, such as `products.name=Keyboard&products.name=Mouse`, match no parent. A filter inside an OR is matched separately, as the next section shows. A to-one relationship has at most one related record, so the rule matters only for to-many relationships. + +Before v5.4.0, different related records could meet these filters. In v5.3.5, the first request returned Acme, a collection `DELETE` with its conditions deleted Acme, and the second request also selected the Dock. + +#### Relationship conditions inside an OR decide parents only + + + +A relationship filter inside an OR (`|`) decides which parents match. It does not narrow which related records `select()` returns: + +```http +GET /Brand/?(products.name=Cable|id=44)&select(id,products{name}) +``` + +This returns Acme with all three of its products (Cable, Keyboard, Dock) and Studio Gear with Stand and Monitor. Without the OR, `products.name=Cable&select(id,products{name})` returns Acme with only its Cable. + +A filter joined with `&` outside the OR still narrows the selection, and a different related record can meet the filter inside the OR: `products.price=ge=number:30&(products.name=Cable|id=44)&select(id,products{name})` returns Acme with Keyboard and Dock (its Cable meets the OR) and Studio Gear with Stand and Monitor. + +Before v5.4.0, a relationship filter inside an OR also narrowed the selection. In v5.3.5, the request above returned Acme with only its Cable, and Studio Gear with an empty `products` array. ### Filtering and Paging Each Relationship :::note Pending release -Targeted for v5.4.0; not released yet. See [Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279) and [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775). +Targeted for v5.4.0; not released yet. See [Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279), [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775), and [Keep an AND group's estimate finite when a sibling condition is filter-only](https://github.com/HarperFast/harper/pull/3724). ::: Use square brackets inside `select()` to apply conditions, sort, and pagination to each parent's related records. -**An outer relationship filter also narrows that relationship's nested results.** For example, `products.price=ge=number:30` selects brands with a qualifying product and restricts their selected products to those costing at least 30. **You do not need to repeat that price filter inside `products[...]`.** Add a nested condition only when you want to narrow the already-eligible products further without removing their parents. +**An outer relationship filter joined with `&` also narrows that relationship's nested results.** A filter inside an OR [does not](#relationship-conditions-inside-an-or-decide-parents-only). For example, `products.price=ge=number:30` selects brands with a qualifying product and restricts their selected products to those costing at least 30. **You do not need to repeat that price filter inside `products[...]`.** Add a nested condition only when you want to narrow the already-eligible products further without removing their parents. The following scenarios use this self-contained schema, including string product IDs and an indexed price field. In a fresh application, use this schema instead of the earlier Brand/Product example; do not combine the two definitions: @@ -539,7 +570,7 @@ With the sample data, the response is: The outer `sort(name,id)` orders brands; the nested `sort(price,id)` orders products within each brand. Use a unique field such as `id` as the final sort key so equal names or prices have a deterministic order. This does not prevent records moving between offset pages when data changes between requests. - An outer sort can also traverse a to-many relationship — for example, `GET /Brand/?products.price=ge=number:30&sort(products.name,id)` — to order parent records by their related values, unlike a nested selection sort (`products[sort(price)]`), which orders the children within each parent and not the parents. Each parent's sort key is the list of its related records' values in relationship order: the related records' primary-key order in the foreign-key index for a `@relationship(to: ...)` field (not sorted by the target attribute or reduced to a min/max), or the stored foreign-key array order for a `@relationship(from: ...)` array. An outer relationship filter such as `products.price=ge=number:30` restricts which parents match and which children `select()` returns, but does not narrow the related records loaded into each parent's sort key: on the sample data above, Acme's `products.name` key is `['Cable', 'Keyboard', 'Dock']` (from `p1`, `p2`, `p3`) and Studio Gear's is `['Stand', 'Monitor']` (`p5`, `p6`), so `sort(products.name,id)` returns Acme (`42`) before Studio Gear (`44`) and `sort(-products.name,id)` reverses that order. Lists compare element by element (`['alpha']` < `['alpha', 'delta']` < `['bravo']`). A parent with no related records has an empty list (`[]`), a related record that omits the sort attribute (or an unresolved ID in a `@relationship(from: ...)` array without `filterMissing`) produces `undefined` at that position, and an explicit `null` attribute produces `null`: both `null` and `undefined` sort before booleans, numbers, and strings (`[]` and `[undefined]` compare as equal because reading past the end of a shorter array yields `undefined`). Across multiple to-many hops, each hop produces a list in relationship order, so two to-many hops compare a nested list of lists per parent (`[]` < `[[], ['a']]` < `[['b']]`). List-per-parent sorting runs in memory when the target attribute is not `@indexed` (with a condition or `allowFullScan: true`), when a more selective condition on another attribute leads the query, or when `enforceExecutionOrder: true` is set, loading the related records for every matched parent; if the target attribute is `@indexed` (such as `products.price`) and no more selective condition on another attribute leads the query, Harper instead drives the scan from the related table's index join (yielding each parent at its first matching index entry and, when unconditional, omitting parents with no related records). Note that, unlike a nested `select` sort, an outer `sort` does not currently enforce `attribute_permissions` on the related sort attribute, so restricting read access to a child attribute does not prevent ordering parent records by it. + An outer sort can also traverse a to-many relationship — for example, `GET /Brand/?products.price=ge=number:30&sort(products.name,id)` — to order parent records by their related values, unlike a nested selection sort (`products[sort(price)]`), which orders the children within each parent and not the parents. Each parent's sort key is the list of its related records' values in relationship order: the related records' primary-key order in the foreign-key index for a `@relationship(to: ...)` field (not sorted by the target attribute or reduced to a min/max), or the stored foreign-key array order for a `@relationship(from: ...)` array. An outer relationship filter such as `products.price=ge=number:30` restricts which parents match and (when joined with `&`) which children `select()` returns, but does not narrow the related records loaded into each parent's sort key: on the sample data above, Acme's `products.name` key is `['Cable', 'Keyboard', 'Dock']` (from `p1`, `p2`, `p3`) and Studio Gear's is `['Stand', 'Monitor']` (`p5`, `p6`), so `sort(products.name,id)` returns Acme (`42`) before Studio Gear (`44`) and `sort(-products.name,id)` reverses that order. Lists compare element by element (`['alpha']` < `['alpha', 'delta']` < `['bravo']`). A parent with no related records has an empty list (`[]`), a related record that omits the sort attribute (or an unresolved ID in a `@relationship(from: ...)` array without `filterMissing`) produces `undefined` at that position, and an explicit `null` attribute produces `null`: both `null` and `undefined` sort before booleans, numbers, and strings (`[]` and `[undefined]` compare as equal because reading past the end of a shorter array yields `undefined`). Across multiple to-many hops, each hop produces a list in relationship order, so two to-many hops compare a nested list of lists per parent (`[]` < `[[], ['a']]` < `[['b']]`). List-per-parent sorting runs in memory when the target attribute is not `@indexed` (with a condition or `allowFullScan: true`), when a more selective condition on another attribute leads the query, or when `enforceExecutionOrder: true` is set, loading the related records for every matched parent; if the target attribute is `@indexed` (such as `products.price`) and no more selective condition on another attribute leads the query, Harper instead drives the scan from the related table's index join (yielding each parent at its first matching index entry and, when unconditional, omitting parents with no related records). Note that, unlike a nested `select` sort, an outer `sort` does not currently enforce `attribute_permissions` on the related sort attribute, so restricting read access to a child attribute does not prevent ordering parent records by it. Keep relationship lookup fields and outer filter fields indexed, and project only the fields the client needs. The sample schema indexes `brandId`, `price`, and `Brand.name`. Nested sorting can use an unindexed child field because it orders the already-related candidates. A child sort may still collect and sort all eligible related records before returning its page; an index on `price` does not guarantee an index-ordered nested scan. Keep parent pages small and consider a separately paginated child endpoint for very large relationships. Unsorted nested limits can stop early, but are appropriate only when child order does not matter. @@ -593,7 +624,7 @@ Acme's matching products sort to Dock ($60), then Cable ($10), so `limit(1,2)` r Use a non-negative safe-integer window; invalid bounds such as negative or fractional values return HTTP 400; `limit(0)` returns no children without reading them; an absent foreign key keeps its missing-value shape. A missing singular relationship remains absent. For array-valued foreign keys, pagination alone preserves missing target positions; adding conditions or a sort drops missing targets. Child conditions and sort fields remain subject to read permissions. -A condition that traverses another relationship qualifies the immediate child; it does not narrow that child's projected descendants. To filter or page grandchildren, put those constraints on the grandchild's own bracket selection. Outer join restrictions are still inherited. +A condition that traverses another relationship qualifies the immediate child; it does not narrow that child's projected descendants. To filter or page grandchildren, put those constraints on the grandchild's own bracket selection. Outer filters joined with `&` are still inherited; one inside an OR is not. For source-backed caches, eligibility and sort keys use local cached entries, as in a top-level table query. Returned children can refresh from source; a refreshed child that no longer matches, or has been deleted, does not consume offset or limit. A locally rejected child is not fetched to discover whether its source value now matches. Ordering is not recomputed from refreshed source values. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 916e1b5af..50ef481b8 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -14,9 +14,9 @@ Full-text indexes using fulltext 0.5.0 with current field-based declarations, or ## Nested Relationship Constraints -[Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279) and [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775) correct REST and programmatic relationship queries that accepted nested constraints, non-leading relationship conditions, or top-level relationship sorts without applying them completely. Filters, ordering, offset, and limit inside `select` apply per parent in collection and single-record reads, and parents remain present when the nested selection has no matches. Outer relationship filters — whether they lead the query or follow another condition — constrain the selected children as well as the parents, so an identical filter does not need to be repeated inside the nested selection. When a top-level sort across a to-many relationship (such as `sort(products.name)`) runs in memory — when the target attribute is not `@indexed`, a more selective condition on another attribute leads the query, or `enforceExecutionOrder: true` is set — Harper now orders matched parent records by the list of each parent's related values in relationship order, compared element by element, instead of ignoring the sort; when an `@indexed` target attribute leads the query, Harper continues to order via the related table's index join. +[Honor nested relationship filters and pagination](https://github.com/HarperFast/harper/pull/3279) and [Fix wrong rows, unfiltered selects and ignored sorts in relationship queries](https://github.com/HarperFast/harper/pull/3775) correct REST and programmatic relationship queries that accepted nested constraints, non-leading relationship conditions, or top-level relationship sorts without applying them completely. Filters, ordering, offset, and limit inside `select` apply per parent in collection and single-record reads, and parents remain present when the nested selection has no matches. Outer relationship filters joined with AND — whether they lead the query or follow another condition — constrain the selected children as well as the parents, so an identical filter does not need to be repeated inside the nested selection. [Keep an AND group's estimate finite when a sibling condition is filter-only](https://github.com/HarperFast/harper/pull/3724) makes two further rules hold consistently: AND-joined filters on the same to-many relationship must all be met by one related record, and a relationship filter inside an OR decides which parents match without narrowing the selected children. When a top-level sort across a to-many relationship (such as `sort(products.name)`) runs in memory — when the target attribute is not `@indexed`, a more selective condition on another attribute leads the query, or `enforceExecutionOrder: true` is set — Harper now orders matched parent records by the list of each parent's related values in relationship order, compared element by element, instead of ignoring the sort; when an `@indexed` target attribute leads the query, Harper continues to order via the related table's index join. -**Upgrade impact:** requests using nested constraints can return fewer children, and requests with top-level sorts across to-many relationships now return parents ordered by their related values. Unsupported filters on plain JSON projections or custom non-table resources, and nested full-text predicates, now return HTTP 400 instead of being ignored. Sorting and pagination of plain JSON arrays remain supported. Nested selections on source-backed caches use the same local-entry eligibility and ordering rules as a top-level query; the correction now applies those rules to constrained nested results. Refreshed nonmatches are excluded from the returned child page, so cache-backed child arrays can also shrink. See [Filtering and Paging Each Relationship](/reference/v5/rest/querying#filtering-and-paging-each-relationship) and [`sort`](/reference/v5/resources/resource-api#sort). +**Upgrade impact:** requests using nested constraints can return fewer children, and requests with top-level sorts across to-many relationships now return parents ordered by their related values. A query or collection `DELETE` whose AND-joined filters on one to-many relationship were met only by different related records no longer matches that parent, and a request with a relationship filter inside an OR can return more parents or children than before. Unsupported filters on plain JSON projections or custom non-table resources, and nested full-text predicates, now return HTTP 400 instead of being ignored. Sorting and pagination of plain JSON arrays remain supported. Nested selections on source-backed caches use the same local-entry eligibility and ordering rules as a top-level query; the correction now applies those rules to constrained nested results. Refreshed nonmatches are excluded from the returned child page, so cache-backed child arrays can also shrink. See [Filtering and Paging Each Relationship](/reference/v5/rest/querying#filtering-and-paging-each-relationship), [Conditions on one relationship match one related record](/reference/v5/rest/querying#conditions-on-one-relationship-match-one-related-record), [Relationship conditions inside an OR decide parents only](/reference/v5/rest/querying#relationship-conditions-inside-an-or-decide-parents-only), and [`sort`](/reference/v5/resources/resource-api#sort). These corrections are not released yet.