SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList - #4761

Merged
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap
Aug 28, 2026
Merged

SOLR-18373: remove NamedList.asShallowMap/get(String,int) and SolrParams.toNamedList#4761
dsmiley merged 8 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18373-remove-namedlist-asshallowmap

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18373

Removes NamedList.get(String,int), asShallowMap()/asShallowMap(boolean), and SolrParams.toNamedList() — 30 call sites, migrated to the documented SimpleOrderedMap(MapWriter) constructor / indexOf+getVal.

Where to look: asShallowMap() was a hybrid, not a view — get/put/remove were live against the backing NamedList, but entrySet()/keySet()/values() returned a depth-1 copy that collapsed duplicate keys. The migration preserves the live-write sites (QueryComponent, CombinedQueryComponent — explicit indexOf then add-or-setVal) and changes duplicate-key handling only where nothing reachable actually has duplicates (SolrXmlConfig, LTRThreadModule).

One place where the documented replacement would have been a bug: SolrJacksonMapper's NamedList serializer used asShallowMap(); handing it a SimpleOrderedMap (which IS a NamedList) would recurse into itself. Uses asMap(0) instead.

Left better than found: the deleted test covered only asShallowMap's write-through; SimpleOrderedMapTest gains the copy-not-view invariant every migrated site now depends on, confirmed by a trap.

Verified: full compile, ecjLint/renderJavadoc on solrj+core, 8 changed test classes — 71 tests, 0 failures.

SOLR-18374, SOLR-18380, SOLR-18386 and SOLR-18389 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley this removes three things you deprecated (NamedList.get(String,int), asShallowMap(), SolrParams.toNamedList()). Worth your eyes specifically: asShallowMap() turned out to be a hybrid, not a plain view -- get/put/remove were live against the backing list but entrySet()/keySet()/values() returned copies. You know this class best, so a review here would help more than most.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 13:22
serhiy-bzhezytskyy added a commit to serhiy-bzhezytskyy/solr that referenced this pull request Aug 21, 2026
Same shape as apache#4763/apache#4761 -- a narrow, single-purpose ClusterState
helper, no observable behavior change.
…ams.toNamedList
Four deprecated members, 30 call sites over five compile rounds.
The replacement for asShallowMap is the SimpleOrderedMap(MapWriter)
constructor, which both deprecation notes point at: NamedList implements
MapWriter, and SimpleOrderedMap extends NamedList and implements Map, so
it can stand in wherever a Map was wanted. get(String, int) becomes
indexOf(name, start) with getVal(idx), which is the same loop -
indexOf's body is character-for-character the removed method's except
that it returns the index. And SolrParams.toNamedList becomes new
SimpleOrderedMap<>(params), whose writeMap applies the identical
String-versus-String[] rule.
What the deprecation notes do not say, and what had to be read out of
the deleted code.
asShallowMap was a hybrid, not a view. get, put, remove, clear and
containsKey were live against the backing NamedList, but entrySet(),
keySet() and values() all returned asMap(1) - a depth-1 COPY that
collapsed duplicate keys into a List and converted nested NamedList
values to Maps. So the migration is behaviour-preserving for the
read-only sites and changes duplicate handling for the ones that stream
over entrySet: SolrXmlConfig would now let Collectors.toMap throw on
duplicate coreAdminHandlerActions entries rather than stringify a
collapsed List, and LTRThreadModule would remove both copies of a
duplicated threadModule key rather than one. Both are arguably fixes;
neither is reachable with the flat scalar values those sites actually
see.
containsKey is the other axis: the removed view's was get(key) != null,
SimpleOrderedMap's is indexOf(key) >= 0, and they differ for a key
present with a null value. Reached only at PackageManager, where a
top-level null params cannot occur.
Two sites were not read-only at all - QueryComponent and
CombinedQueryComponent both did getResponseHeader().asShallowMap().put(...),
i.e. a write-through. They now do the same thing explicitly, indexOf
then add-or-setVal, which is what the deleted put did. Neither
setPartialResults (add-only-if-absent, and the key may already hold
"omitted") nor the file's neighbouring remove-then-add idiom is
equivalent, the latter because it also moves the key to the end of the
header and changes serialized order.
Two things that looked like format risks and were not, both settled by
reading rather than assuming. JavaBinCodec writes ORDERED_MAP for a
SimpleOrderedMap and NAMED_LST for a plain NamedList, so swapping the
type inside an update request or a response header looks like a wire
change - but the removed toNamedList() already constructed a
SimpleOrderedMap, so the tag was already ORDERED_MAP. And
SolrParams.writeMap is the canonical serialisation used everywhere
else; it differs from the removed method only in skipping a parameter
whose value array is empty, which toNamedList() emitted as an empty
array.
One site where the documented replacement would have been a bug.
SolrJacksonMapper registers a StdSerializer<NamedList> and did
writeObject(value.asShallowMap()); handing it a SimpleOrderedMap, which
IS a NamedList, would dispatch straight back into the same serializer
forever. It uses asMap(0) instead - a plain LinkedHashMap that leaves
nested NamedLists for Jackson to dispatch one level at a time, which is
what the anonymous Map did.
Left better than found. NamedListTest.testShallowMap tested only the
removed method's write-through and is deleted; in its place
SimpleOrderedMapTest gains the invariant every migrated call site now
depends on - that the MapWriter constructor copies, so mutating the
copy does not reach the source and adding to the source does not reach
the copy. A trap confirms it is not vacuous: asserting view semantics
instead fails exactly that test, 1 of 17, with zero compile errors.
DefaultSchemaSuggester needed no wrapper at all: fieldProps is already
a SimpleOrderedMap, so dropping .asShallowMap() passes the same
instance and even preserves the write-through.
Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain and ecjLintTest on solrj and core,
renderJavadoc on both, and every changed test class - 8 classes, 71
tests, 0 failures, 1 skipped.
Two findings parked rather than touched, both pre-existing:
SolrQueryResponse.getResponseHeader declares NamedList<Object> while
its body casts to SimpleOrderedMap<Object>, so widening that return
type would collapse both write-through hunks to one line each - but it
is a public and binary-incompatible API change, so it belongs to its
own ticket. And PackageManager tests a top-level "params" key while
SolrConfigHandler puts the paramset under "response", so
packageParamsExist appears to be permanently false.
AI-assisted (Claude Sonnet 5)
Same shape as apache#4763 (David: not changelog-worthy) -- narrow, rarely-used
NamedList/SolrParams methods, no observable behavior change.
@serhiy-bzhezytskyy
serhiy-bzhezytskyyforce-pushed the SOLR-18373-remove-namedlist-asshallowmap branch from bad3f17 to a6e287bCompareAugust 22, 2026 05:17
Comment threadsolr/core/src/java/org/apache/solr/handler/component/QueryComponent.java Outdated
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
new SimpleOrderedMap<>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no; do not use SimpleOrderedMap as a general purpose Map. It's not documented well but we should only be creating new ones when we are writing response data structures for efficiency reasons.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to a plain loop over the NamedList into a LinkedHashMap, no SimpleOrderedMap involved.

Comment on lines +73 to +74
// Not SimpleOrderedMap: it IS a NamedList, so this serializer would recurse on it.
gen.writeObject(value.asMap(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate with more words here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded: SimpleOrderedMap extends NamedList, so this serializer would recurse into it infinitely if used here; asMap(0) returns a plain LinkedHashMap at the top level while leaving any nested NamedList values untouched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it seems there is a lost opportunity here to be more efficient, as we're creating a new data structure merely to write it out, when Jackson surely knows how to serialize a Map. I'm not sure if there's a way for us to get Jackson to write it as a Map, bypassing the NamedList detection (avoid infinite recursion).

CC @gerlowskija

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve this right now, the comment should recognize this sad situation so a future reader sees the opportunity / issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented it: registered a second, more specific serializer for SimpleOrderedMap that writes it directly via entrySet(), no copy, no recursion (verified with a standalone Jackson probe before touching this code). Nested plain NamedLists still fall through to the existing asMap(0) path.
Added SolrJacksonMapperTest.

Comment threadsolr/core/src/java/org/apache/solr/packagemanager/PackageManager.java Outdated
…p SimpleOrderedMap misuse, clarify comments
- QueryComponent/CombinedQueryComponent: the indexOf+if/else+setVal upsert
is just remove(key)+add(key,val) -- same effect, no branch.
- SolrXmlConfig/PackageManager: stop using SimpleOrderedMap as a generic
Map adapter (that's not what it's for). SolrXmlConfig builds the map
directly off NamedList's own Iterable<Map.Entry>; PackageManager reads
the NamedList value/key directly instead of wrapping it first.
- SolrJacksonMapper: expanded the comment explaining why SimpleOrderedMap
specifically (not just "a NamedList") would recurse here.
- JavaBinUpdateRequestCodec: reworded a comment that referenced "as
before" (PR-review language) to instead state the actual reason --
JavaBinCodec picks the wire tag from the runtime type, and receivers
expect ORDERED_MAP here.
… upsert
getResponseHeader() is always a SimpleOrderedMap at runtime, which already has
an in-place put(): indexOf + setVal/add. That replaces the remove()+add() pair
without reordering the entry to the end of the header.
@dsmiley

Copy link
Copy Markdown
Contributor

note: precommit failed but for a reason I believe that should go away if you sync from main.

…lper
getResponseHeader()'s documented contract is NamedList<Object>, not
SimpleOrderedMap -- the previous commit's cast broke
QueryComponentPartialResultsTest, whose MockResponseBuilder stubs
getResponseHeader() to return a plain NamedList via Mockito, fully within
that contract.
Reverted to remove()+add(), and extracted it into a shared
updateResponseHeader() on QueryComponent, reused by CombinedQueryComponent
(which extends it) for both the partialResults site and the
segmentTerminatedEarly upsert that already used the same pattern.
… copy
SimpleOrderedMap already implements Map, so the copy-to-LinkedHashMap step
asMap(0) does before handing it to Jackson is unnecessary for that case --
it was only there to dodge the infinite recursion a SimpleOrderedMap would
otherwise cause in NamedListSerializer (SimpleOrderedMap extends NamedList).
Registered a second, more specific serializer for SimpleOrderedMap that
writes it out via its own entrySet(), delegating each value back through
the provider so nested NamedLists/SimpleOrderedMaps still get whichever
serializer actually matches their runtime type. defaultSerializeField()
doesn't honor the mapper's NON_NULL inclusion on its own, so null values
are skipped explicitly to match how the NamedListSerializer path already
behaves.
Verified with a real end-to-end test (SolrJacksonMapperTest) covering the
direct SimpleOrderedMap case, a plain NamedList nested inside it (which
still needs the asMap(0) path), and null-value omission on both paths.
header.remove(key);
header.add(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

…tract
Per dsmiley: MockResponseBuilder's mock returning a plain NamedList was the
actual bug, not the documented contract. Made it return a real
SimpleOrderedMap and restored the cast+put() in updateResponseHeader.
Also: unused hamcrest assertThat import in SolrJacksonMapperTest was
already failing ecjLint on this branch (unrelated to this change).
@dsmiley
dsmiley merged commit 55e3b88 into apache:mainAug 28, 2026
5 of 7 checks passed
@dsmileydsmiley added this to the 10.x milestone Aug 28, 2026
dsmiley pushed a commit that referenced this pull request Aug 29, 2026
…ams.toNamedList (#4761)
And improve performance of the V2 API for serializing SimpleOrderedMap (custom Jackson serializer).
(cherry picked from commit 55e3b88)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@dsmiley@epugh