[bugfix] Sequence.containsReference: recurse into nested map/array items#6507
[bugfix] Sequence.containsReference: recurse into nested map/array items#6507joewiz wants to merge 3 commits into
Conversation
The general-purpose value sequences checked only direct reference equality
(item == values[i]) and did not recurse into items that are themselves
containers, unlike MapType.containsReference and ArrayType.containsReference
which both do `value == item || value.containsReference(item)`. Affected:
ValueSequence, ArrayListValueSequence, OrderedValueSequence,
PreorderedValueSequence, and SubSequence. (NodeSets hold only nodes and
RangeSequence only integers, so neither can nest a map/array; both are
unaffected.)
As a result, a value nested inside a map (or array) held by such a sequence
was invisible to the variable-cleanup guard: when a let/function scope pops,
XQueryContext.popLocalVariables -> VariableImpl.destroy ->
<sequence>.destroy -> <value>.destroy(context, contextSequence) skips closing
a value only if `contextSequence.containsReference(value)`. For a file-backed
binary returned nested in a map inside a sequence, that guard returned false,
so the binary's file channel was closed while still referenced through the
returned map. A later read then failed with "Underlying channel has been
closed", surfaced (e.g.) as "error while obtaining length of binary value"
from xmldb:store.
This is exactly the path the Roaster / existdb-openapi multipart upload
tripped: request:get-uploaded-file-data, carried as $request?body?file?data
(Roaster's form-data binary shape: map { name, data, size }), failed at
xmldb:store. Reproduced with the real Roaster body:parse and reduced to the
test cases below.
Fix: make each general-purpose sequence's containsReference recurse into
container items, mirroring MapType/ArrayType.
Tests:
- ContainsReferenceNestedTest (unit): directly asserts ValueSequence,
ArrayListValueSequence and SubSequence detect an item nested in a map
(fails without the fix). OrderedValueSequence/PreorderedValueSequence take
the identical one-line fix (their OrderSpec construction is not exercised
directly).
- binary-nested-in-map.xqm (XQSuite, file module): a file:read-binary value
returned nested in a map inside a sequence and escaping a function, then
read; fails with "Underlying channel has been closed" before the fix. The
inner one-iteration FLWOR is load-bearing -- it wraps the map in a
ValueSequence; a bare map { ... } goes through MapType.containsReference,
which already recurses, and would not exercise the bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ta response)
cy.request with a FormData body uploads the bytes correctly (the package
installs server-side) but returns an empty `{}` response body, so the install
result was lost and the multipart assertions failed. Switch the upload helper to
the browser's native fetch, which runs same-origin to baseUrl and returns the
real JSON response. uploadXar() now resolves to a normalized { status, body }.
All 13 packages.cy.js cases pass against a build carrying eXist-db/exist#6507
(the core fix for the file-backed binary closed across nested map/array items).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| @Test | ||
| public void valueSequenceDetectsItemNestedInMap() throws Exception { | ||
| withMapNesting((map, nested, notNested) -> { |
There was a problem hiding this comment.
Can this be rewritten to
final ValueSequence vs = withMapNesting() (map, nested, notNested) -> {
final ValueSequence vs = new ValueSequence();
vs.add(map);
return vs;
});
assertTrue(vs.containsReference(nested));
assertFalse(vs.containsReference(notNested));There was a problem hiding this comment.
I have to revise my earlier review, the tests use some "clever" construct for testing that is hard to follow.
Preferrably, there is a simpler more straightforward solution. This can be more verbose and duplicate more code as long as it is easy to follow.
line-o
left a comment
There was a problem hiding this comment.
I would like to see Codacy to like the new junit tests. But this is not a blocker for me.
…erface helper Per review feedback on PR eXist-db#6507: replace the withMapNesting(...) lambda indirection with straightforward, inlined per-test setup. More verbose and slightly duplicated, but easier to follow at a glance. No change to coverage: each test still builds map { "data": $nested } in a fresh context, wraps it in the sequence type under test, and asserts the nested item is detected while a sibling item is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
[This response was prompted by Joe, drafted by Claude Code, and reviewed by Joe.] Thanks @line-o. Both addressed in be7e255:
|
… & Install Move package installs onto existdb-openapi's multipart endpoint (POST /api/packages/install, the .xar in the "file" part), dropping xst: - Package Manager: enable the previously-disabled "Install .xar…" — pick a local .xar (via Oxygen's own file chooser, so it honors the "Use platform file chooser" preference) and upload it; refresh the list; report name/version. - Build & Install (Project pane): install the built .xar via the same multipart upload instead of shelling out to `xst package install`. No external tool needed; credentials come from the resolved saved connection. - ExistClient.installPackageFile now parses the success response and returns InstalledPackage(name, version, target); throws IOException with the server message on failure. - Package Manager shows an indeterminate progress spinner while a server op (install/update/remove/local-.xar) runs — large packages take time. Requires an existdb-openapi build whose packages:install reads the multipart file part (consolidated upload handler + eXist-db/exist#6507). Verified end-to-end against the trio. README + the Build & Install docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| declare | ||
| %test:assertEquals("SERVER_SECRET=123!") | ||
| function bnm:binary-nested-in-map-survives-function-return() { | ||
| let $directory := helper:get-test-directory($bnm:suite) |
There was a problem hiding this comment.
Lines 64 and 65 should be in a setup function.
There was a problem hiding this comment.
[This response was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
Done in 9a11664 — fixture creation now lives in a %test:setUp function, symmetric with the %test:tearDown. One wrinkle worth noting: helper:get-test-directory embeds a fresh util:uuid() on every call, so computing the directory independently in setUp and in the test would put the fixture and the read in different directories. I compute it once in a module variable ($bnm:directory) that both share. FileTests stays green.
Per review feedback on PR eXist-db#6507: the fixture setup (create the test directory and write the .env fixture) now lives in a %test:setUp function, symmetric with the existing %test:tearDown, instead of inline in the test body. helper:get-test-directory embeds a fresh uuid on every call, so the directory is computed once in a module variable ($bnm:directory) shared by setUp and the test, rather than recomputed per function (which would land the fixture and the read in different directories). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
[This PR was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
Summary
A file-backed binary value (
BinaryValueFromFile, e.g. fromrequest:get-uploaded-file-dataorfile:read-binary) that is nested inside a map (or array) held by a sequence could have its file channel closed while it was still referenced — after which any read failed with "Underlying channel has been closed". The most visible symptom: a multipart upload stored through a routing framework (Roaster / existdb-openapiPOST /api/db/.../install) failed atxmldb:storewith:Root cause
The general-purpose value sequences checked only direct reference equality and did not recurse into items that are themselves containers — unlike
MapType.containsReferenceandArrayType.containsReference, which both dovalue == item || value.containsReference(item):containsReferenceis the guard eXist uses to avoid destroying a value that is part of a result. When alet/function scope pops,XQueryContext.popLocalVariables→VariableImpl.destroy→<sequence>.destroy→<value>.destroy(context, contextSequence), andBinaryValueFromFile.destroyonly skips closing whencontextSequence.containsReference(this). For a binary returned nested in a map inside a sequence, that check returnedfalse, so the channel was closed even though the value was still reachable through the returned map.Five general-purpose sequence types had this gap:
ValueSequence,ArrayListValueSequence,OrderedValueSequence,PreorderedValueSequence, andSubSequence— any of which can be thecontextSequencewhile holding a map/array. (MapType/ArrayTypealready recurse correctly;RangeSequenceholds only integers and everyNodeSetholds only nodes, so none of those can nest a map/array.)This is precisely the path the Roaster / existdb-openapi multipart upload tripped:
request:get-uploaded-file-datais carried as$request?body?file?data(Roaster's form-data binary shape,map { name, data, size }produced byfor ... return map { "data": $data[$index] }), and the deferred handler'sxmldb:storethen read a value whose channel had already been closed by the enclosing function's variable cleanup.Fix
Make each affected sequence's
containsReferencerecurse into container items, mirroringMapType/ArrayType:How it was found
Reproduced end-to-end against the real Roaster
body:parsedeployed into a local eXist, instrumentingBinaryValueFromFileto capture the close-site. The close fired frompopLocalVariables → destroywithcontainsReferencereturningfalseon aValueSequenceof maps; the fix flips it totrueand the upload stores cleanly. Reduced to a pure-XQuery / direct unit-test case, then acontainsReferenceaudit of everySequenceimplementation found the four sibling types with the identical gap.What changed
exist-core/.../xquery/value/{ValueSequence, ArrayListValueSequence, OrderedValueSequence, PreorderedValueSequence, SubSequence}.java—containsReferencerecurses into nested container items.Test plan
ContainsReferenceNestedTest(unit): assertsValueSequence,ArrayListValueSequenceandSubSequencedetect an item nested in a map (and don't report an unrelated item). Each fails without the fix.OrderedValueSequence/PreorderedValueSequencetake the identical one-line change (theirOrderSpecconstruction is not exercised directly).FileBinaryNestedInMapTest(file module): afile:read-binaryvalue returned nested in a map inside a sequence from a function, then read. Fails with "Underlying channel has been closed" before the fix, passes after.Notes
BinaryValueFromFileshared-reference refcounting, the enclosed-expression premature-close path). Both are "file-backed binary closed while still referenced", but distinct root causes in different files; this one is the multipart/xmldb:storesymptom. Neither fixes the other; both are needed.