Bound the tree children query and CONSTRUCT only what a node renders - #390
Merged
Merged
Conversation
ldh:tree-children-query emitted an unbounded DESCRIBE ?child, so opening the drawer tree fetched the full description of every child of a container. It fires on every page render, not on a click: ldh:NavigationUpdate walks down to the current document and fetches each level on the way. Measured against the Rebrickable dataspace (~158k documents, one entity per named graph): container children DESCRIBE CONSTRUCT taxonomies/colors/ 275 202 KB - sets/ 28,356 20.5 MB 6.6 MB parts/ 64,647 502 18.1 MB Both large containers exceeded the platform response limit, so the tree answered 502 on every page beneath them. A node renders four things - @Rdf:about, a label, rdf:type for the icon, and whether the tree relation is present for the disclosure - so a CONSTRUCT of just those is both correct and several times smaller. Size independent of container size needs the bound as well: ldh:tree-page-size (default 1000) limits the child page, and an app stylesheet can raise it. A bound alone would break the descent. ldh:doctree-descend picks the next step out of the rendered children and falls through silently when it is absent, which with a LIMIT it usually would be. The query now takes the document being opened to and includes the child leading to it whatever else the page holds, so the tree still opens to and highlights the current document. Verified on that dataspace: sets/ 343 KB (was 20.5 MB), parts/ and inventories/ 200, the tree opens to parts/3001/, and the SKOS trees from the editor/taxonomy package inherit the same bound unchanged - they call this function, and their top concepts and narrower concepts both fetch through the new CONSTRUCT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ound
Three places matched the children query by its verb, and bounding it broke all
of them - two silently, which is the reason this is its own commit rather than a
line in the last one.
tree-children-failure CHILDREN_QUERY routes the injected 403. A route pattern
that matches nothing fails open: the refusal never
happens and every assertion passes against a tree that
was never refused anything.
concept-tree counts.down stopped counting, so "the DOM is the cache"
compared 0 against 0 and passed asserting nothing.
lib/console the allowance for the 502 the unlimited DESCRIBE
produced. That 502 is gone, so the entry became a blind
spot over exactly the request the fix was about, and is
removed rather than updated.
Both patterns now match the projection rather than the verb: CONSTRUCT alone is
not distinctive, where `?child a ?Type` is only ever this query.
Two specs cover what the fix introduced:
asks for a bounded page of children, and only what a node renders - a LIMIT is
present and a DESCRIBE is not. Both halves matter: a CONSTRUCT that lost its
bound would still grow without limit, and a bounded DESCRIBE would still carry
every triple of every child it returned.
opens the path even when it falls outside the page of children - rewrites the
LIMIT to 0 on the wire, so a small fixture container behaves like one holding
more children than a fetch returns. The bound applies to the inner subselect
alone, so at zero the page contributes nothing and every row still rendered
got there through the path branch.
Verified against a 64k-child container, including the negative control that
makes the second spec worth having:
baseline container expanded, target active, 1001 children
LIMIT 0 container expanded, target active, 1 child
LIMIT 0, path branch removed no container row, target absent, not active
So the spec passes for the reason it claims to, and fails when the branch it is
written about is taken away.
The suite itself was not run end to end here: it wants an instance where nothing
is readable by default, and the stack available to me grants anonymous read
across the dataspace, which its preflight rejects by design.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
ldh:tree-children-queryemitted an unboundedDESCRIBE ?child, so opening the drawer tree fetched the full description of every child of a container. It fires on every page render, not on a click —ldh:NavigationUpdatewalks down to the current document and fetches each level on the way.Measured against a dataspace of ~158k documents, one entity per named graph:
taxonomies/colors/sets/parts/Both large containers exceeded the response limit, so the tree answered 502 on every page beneath them.
The fix
A node renders four things —
@rdf:about, a label,rdf:typefor the icon, and whether the tree relation is present for the disclosure — so a CONSTRUCT of just those is both correct and several times smaller. Making the size independent of container size needs the bound as well:ldh:tree-page-size(default 1000), which an app stylesheet can raise.A bound alone would break the descent.
ldh:doctree-descendpicks its next step out of the rendered children and falls through silently when it is absent, which with a LIMIT it usually would be. The query now takes the document being opened to and includes the child leading to it whatever else the page holds.Scope
The SKOS trees from the
editor/taxonomypackage call this same function and inherit the bound with no package change. Verified on a live instance: top concepts and narrower concepts both fetch through the new CONSTRUCT.The package's own ancestor-path walk is deliberately left alone — it is a
SELECT DISTINCT ?parentover aVALUESfrontier, already minimal, and a LIMIT there would silently stop the tree opening to the target.Tests
Three places in
ui-testsmatched the query by its verb and broke — two of them silently, which is why they are a separate commit:tree-children-failureroutes the injected 403 on that pattern; a route matching nothing fails open, so the refusal never happened and every assertion passed against a tree that was never refused anything.concept-tree'scounts.downstopped counting, so the DOM is the cache compared 0 against 0.lib/consoleallowed the 502 the unlimited DESCRIBE produced — now removed rather than updated, since an allowance that outlives its defect is a blind spot over exactly the request this changes.Two new specs cover the fix, with the negative control that makes the second worth having:
LIMIT 0LIMIT 0, path branch removedThe suite was not run end to end: it requires an instance where nothing is readable by default, and the stack available granted anonymous read across the dataspace, which its preflight rejects by design.
🤖 Generated with Claude Code