Skip to content

[refactor] Always sort internal-module FunctionDef[]; drop functionsOrdered flag (#6378)#6384

Merged
line-o merged 2 commits into
eXist-db:developfrom
joewiz:bugfix/6378-drop-ordered-flag
May 20, 2026
Merged

[refactor] Always sort internal-module FunctionDef[]; drop functionsOrdered flag (#6378)#6384
line-o merged 2 commits into
eXist-db:developfrom
joewiz:bugfix/6378-drop-ordered-flag

Conversation

@joewiz
Copy link
Copy Markdown
Member

@joewiz joewiz commented May 17, 2026

Summary

Closes #6378. Implements Option 2 per @duncdrum's preference on the issue thread:

FWIW I m leaning heavily towards 2. There is a whole cluster of bugs we encountered recently where inconsistent sorting leads to hard to debug and nasty bugs.

The functionsOrdered=true opt-in flag on AbstractInternalModule's 3-arg constructor was an unenforced precondition: callers had to pass a FunctionDef[] already sorted by FunctionId order, or binary search in getFunctionDef() would silently miss entries. The footgun manifested as a real bug in #6376 (cursor:close unreachable via function-lookup but visible via util:registered-functions).

The fix removes the flag entirely. The base class always sorts (defensive-copy + Arrays.sort in the constructor) and always uses binary search. Five first-party modules that previously carried static { Arrays.sort(functions, new FunctionComparator()); } boilerplate no longer need it.

What changed

Commit 1 — base class (AbstractInternalModule.java, +20 / -24):

  • Remove the 3-arg (FunctionDef[], Map, boolean) constructor.
  • 2-arg constructor now defensive-copies and sorts (caller's static final array left intact).
  • Remove the ordered field and the conditional in getFunctionDef(); binary search is unconditional.
  • Reorder field declarations above the FunctionComparator inner class (PMD FieldDeclarationsShouldBeAtStartOfClass).

Commit 2 — 20 callers + 5 static-sort cleanups (+167 / -50):

Callers updated (drop the third argument):

Module Was
BackupModule, FnModule, InspectionModule, RequestModule, UtilModule, XMLDBModule, ContentExtractionModule, ExiftoolModule, XQDocModule, CounterModule, ProcessModule super(functions, parameters, true)
ArrayModule, MapModule, WebSocketModule, ConsoleCompatModule, VectorModule, LuceneModule, SortModule, NGramModule, RangeIndexModule super(functions, parameters, false)

Static-sort blocks removed (static { Arrays.sort(functions, new FunctionComparator()); }) from XMLDBModule, UtilModule, RequestModule, FnModule, CounterModule — plus the now-unused java.util.Arrays imports.

Net: ~34 lines of boilerplate gone from the codebase.

Regression test (AbstractInternalModuleSortTest, new — 4 cases):

API break

The 3-arg AbstractInternalModule(FunctionDef[], Map<String, List<?>>, boolean) constructor goes away. Any third-party module passing super(functions, parameters, true|false) needs to switch to super(functions, parameters). One-line change per module. The FunctionComparator static inner class remains public for any code that uses it elsewhere.

@duncdrum explicitly accepted this trade-off on the issue: removing the flag is what makes the bug structurally impossible.

Performance

The sort is O(N log N) once per module instance construction. eXist's largest module is FnModule (~500 functions), sorted in microseconds. AbstractInternalModule is instantiated per XQueryContext, which is per-query — so the sort runs per query. For comparison, the previous static { Arrays.sort(...) } blocks ran once at class load. The cost is small but real; happy to memoize per-class if reviewers want to eliminate it entirely (one-time check via ConcurrentMap<Class<?>, Boolean>).

Test plan

  • mvn test -pl exist-core -Dtest=AbstractInternalModuleSortTest — 4/4 pass
  • mvn compile across exist-core + extensions (minus exist-xqts, which has unrelated GitHub Packages auth issue locally) — green
  • Codacy clean on changed files
  • CI green

Related

@joewiz joewiz requested a review from a team as a code owner May 17, 2026 14:43
Copy link
Copy Markdown
Contributor

@duncdrum duncdrum left a comment

Choose a reason for hiding this comment

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

very nice

@duncdrum duncdrum requested a review from a team May 17, 2026 18:41
@duncdrum
Copy link
Copy Markdown
Contributor

we should audit our org to see if any custom extensions where using the 3 arg version

@joewiz
Copy link
Copy Markdown
Member Author

joewiz commented May 18, 2026

[This response was co-authored with Claude Code. -Joe]

Org-wide audit done via gh search code across all repos in the eXist-db org. Three-arg super(functions, parameters, true|false) calls live in 6 modules across 5 external repos:

Repo Module Flag value Static sort block?
existdb-openapi ApiModule.java true
existdb-openapi LspModule.java true
exist-http-client HttpClientModule.java true
exist-request RequestModule.java true
exist-stanford-nlp StanfordNLPModule.java false
monex ConsoleModule.java false

None of these carry a static { Arrays.sort(functions, new FunctionComparator()); } block, so the fix per module is a single line: drop the third argument.

13 other external repos that extend AbstractInternalModule already use the 2-arg form — no action needed: exist-alt-identifier, exist-apps-archetype (the official module archetype/template — already correct), exist-binary, exist-commons-text, exist-file, exist-zip, expath-bin-module, expath-crypto-module, jfreechart, messaging-replication (3 modules), xquery-versioning-module, xquery-xproc-xmlcalabash-module.

All static-sort blocks in the org are inside this repo — the five removed in this PR (FnModule, UtilModule, XMLDBModule, RequestModule, CounterModule) are the complete set. No external repo carries that boilerplate.

Coordination options for the 6 affected modules:

  1. Companion PRs in each repo, opened concurrently with this one. Each is a one-line change. I can draft all 6 if helpful. They can sit unmerged until this PR lands, then merge in sequence with a fresh eXist 7-SNAPSHOT dep.
  2. Wait for this PR to merge first, then open the 6 companion PRs. Cleaner causal narrative ("eXist removed the flag, here's the matching update"). Slightly longer total window where those repos can't build against eXist develop.
  3. Land this PR alongside eXist 7.0 release, since most of these external repos are typically updated only against released eXist versions anyway. Some may not have active CI against develop.

My lean is (1) or (2) — the change is trivial, doing it sooner avoids forcing external maintainers to scramble. Happy to take on drafting the 6 PRs.

@duncdrum
Copy link
Copy Markdown
Contributor

1 the sooner we open the PR the better

@joewiz
Copy link
Copy Markdown
Member Author

joewiz commented May 18, 2026

[This response was co-authored with Claude Code. -Joe]

Companion PRs open across the 5 affected external repos (Option 1 per your preference):

Repo PR Modules
existdb-openapi eXist-db/existdb-openapi#21 ApiModule, LspModule
exist-http-client eXist-db/exist-http-client#3 HttpClientModule
exist-request eXist-db/exist-request#2 RequestModule
exist-stanford-nlp eXist-db/exist-stanford-nlp#347 StanfordNLPModule
monex eXist-db/monex#384 ConsoleModule

Each is the same one-line change: drop the third argument from super(functions, parameters, true|false). PR bodies cross-link back to this PR + #6378 + #6376 so future readers can trace the upstream context.

Expected behavior: each companion PR will fail CI until this PR merges and a fresh 7.0.0-SNAPSHOT propagates — they're extending a class whose 3-arg constructor no longer exists. That's the queued-companion shape, not a regression.

Once this PR merges, the 5 companions are ready to follow.

@duncdrum duncdrum added this to v7.0.0 May 19, 2026
@duncdrum duncdrum moved this to In progress in v7.0.0 May 19, 2026
@duncdrum duncdrum requested a review from line-o May 19, 2026 19:11
@duncdrum
Copy link
Copy Markdown
Contributor

duncdrum commented May 19, 2026

@line-o curious to hear what you think. I d like to see this breaking in the beta so that folks have a chance to catch it.

I also think it ll solve a whole class of related downstream bugs.

@line-o
Copy link
Copy Markdown
Member

line-o commented May 19, 2026

Agreed. We missed the Chance to deprecate this in the last 6.4.1 release. Next best thing is to allow to discover it now.
And the possibility to add functions unsorted and hide them that way from discovery should be stopped.

@line-o
Copy link
Copy Markdown
Member

line-o commented May 19, 2026

Wait! @joewiz @duncdrum I just realizd that the 3-arg constructor is removed rather than marked deprecated and the third parameter simply ignored.
What was the reason to break hard instead of just doing the right thing and communicating this openly?

@duncdrum
Copy link
Copy Markdown
Contributor

@line-o because this is mainly a bug fix. The ability to work with unsorted values should never have been there.

3arg true is the new default.
Old 2arg or 3arg false are buggy they shouldn't be deprecated but fixed.

Copy link
Copy Markdown
Member

@line-o line-o left a comment

Choose a reason for hiding this comment

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

Please reinstate a 3-argument constructor and mark it as deprecated.

The third parameter is ignored and all functions will always be sorted.

joewiz and others added 2 commits May 19, 2026 22:45
…recate functionsOrdered flag

Closes eXist-db#6378

The functionsOrdered=true constructor flag opted modules into binary
search at the cost of an unenforced precondition: callers had to pass
a FunctionDef[] already sorted by FunctionId order. Five first-party
modules learned to satisfy the contract via boilerplate static blocks
(`static { Arrays.sort(functions, new FunctionComparator()); }`).
Third-party modules unaware of the convention (e.g. existdb-openapi's
cursor module, see eXist-db#6376) silently lost function-lookup reachability
when their declaration order happened to violate the binary-search
contract.

Per duncdrum's preference on eXist-db#6378, the broken-when-unsorted behavior
is removed. Per line-o's review of eXist-db#6384, the 3-arg constructor is
retained as @deprecated rather than removed outright, so external
modules continue to compile against eXist 7 without source changes —
just a deprecation warning.

Changes to AbstractInternalModule.java:
- 2-arg constructor (FunctionDef[], Map) now defensive-copies + sorts
  the array. The caller's static final array is left intact.
- 3-arg constructor (FunctionDef[], Map, boolean) marked @deprecated
  (since="7.0.0", forRemoval=true). The boolean parameter is ignored;
  the constructor delegates to the 2-arg form.
- Remove the `ordered` field and the `if (ordered)` branch in
  getFunctionDef(); binary search is now unconditional.
- Reorder field declarations above the FunctionComparator inner class
  per PMD FieldDeclarationsShouldBeAtStartOfClass.

Sort cost: O(N log N) once per module instance construction; trivial
at typical module sizes (eXist's largest is FnModule with ~500
functions, sorted in microseconds).

Tests (AbstractInternalModuleSortTest):
- unsortedDeclarationOrderStillFindsAllFunctions — the eXist-db#6376
  reproducer, now safe by construction
- callerArrayIsNotMutated — verifies the defensive copy
- alreadySortedArrayFindsAllFunctions — common-case sanity
- sameQnameDifferentAritiesAllFound — secondary sort key honored
- deprecatedThreeArgConstructorStillWorks — backwards-compat: an
  external module compiled against pre-eXist-db#6378 eXist that still uses
  super(functions, parameters, true|false) compiles and produces a
  functioning module via the @deprecated delegating constructor

Caller updates and removal of redundant static-sort blocks ship in
the follow-up commit in the same PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uctors

Follow-up to the AbstractInternalModule deprecation (eXist-db#6378). The
boolean third argument has no effect — the base class always sorts.
20 callers switch to the cleaner 2-arg form so the codebase doesn't
keep emitting deprecation warnings against itself, and the 5 modules
that carried a `static { Arrays.sort(functions, new FunctionComparator()); }`
block no longer need it (the base class does this work); remove the
blocks and the now-unused java.util.Arrays imports.

Callers updated (20 files):
- exist-core: BackupModule, FnModule, InspectionModule, RequestModule,
  UtilModule, XMLDBModule, ArrayModule, MapModule, WebSocketModule,
  ConsoleCompatModule.
- extensions: ContentExtractionModule, ExiftoolModule, XQDocModule,
  CounterModule, ProcessModule, VectorModule, LuceneModule,
  SortModule, NGramModule, RangeIndexModule.

Static-sort blocks removed (5 modules): XMLDBModule, UtilModule,
RequestModule, FnModule, CounterModule.

External modules compiled against pre-eXist-db#6378 eXist that still use the
deprecated 3-arg constructor will continue to compile against eXist 7
with a deprecation warning, no source changes required.

Net: -34 lines of boilerplate across the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joewiz joewiz force-pushed the bugfix/6378-drop-ordered-flag branch from 2c19768 to 95a94d6 Compare May 20, 2026 02:45
@joewiz
Copy link
Copy Markdown
Member Author

joewiz commented May 20, 2026

[This response was co-authored with Claude Code. -Joe]

Thanks @line-o — done in 15181112fc. The 3-arg AbstractInternalModule(FunctionDef[], Map, boolean) constructor is reinstated and marked @Deprecated(since = "7.0.0", forRemoval = true). The functionsOrdered parameter is documented as ignored; the constructor delegates to the 2-arg form, which always sorts.

External modules compiled against pre-#6378 eXist that still use super(functions, parameters, true) or super(functions, parameters, false) will continue to compile against eXist 7 with a deprecation warning, no source changes required. Verified with a new deprecatedThreeArgConstructorStillWorks test in AbstractInternalModuleSortTest that constructs a module via the legacy 3-arg form with both flag values and confirms function lookup still works.

The internal callers (20 first-party modules) still drop the spurious third arg in the second commit of this PR — they don't need the deprecation path since they're in the same repo. Keeps the codebase from emitting deprecation warnings against itself.

The 5 external companion PRs we opened (existdb-openapi#21, exist-http-client#3, exist-request#2, exist-stanford-nlp#347, monex#384) are no longer strictly necessary — those repos would compile against this PR with just a warning. They're still worth merging as cleanup (drop the spurious flag), just not urgent. I'll re-comment on each to reflect this.

@line-o line-o self-requested a review May 20, 2026 08:48
Copy link
Copy Markdown
Member

@line-o line-o left a comment

Choose a reason for hiding this comment

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

Thanks for the change, I believe that will make our life easier.

@line-o line-o merged commit 8b90e27 into eXist-db:develop May 20, 2026
9 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Done in v7.0.0 May 20, 2026
duncdrum pushed a commit to eXist-db/exist-http-client that referenced this pull request May 21, 2026
…onstructor

eXist-db/exist#6378 (PR eXist-db/exist#6384) removes the 3-arg
`AbstractInternalModule(FunctionDef[], Map, boolean)` constructor.
The 2-arg form is now the only path; the base class always sorts
the function table and always binary-searches.

This commit drops the now-removed third argument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
duncdrum pushed a commit to eXist-db/existdb-openapi that referenced this pull request May 21, 2026
…onstructor

eXist-db/exist#6378 (PR eXist-db/exist#6384) removes the 3-arg
`AbstractInternalModule(FunctionDef[], Map, boolean)` constructor.
The 2-arg form is now the only path; the base class always sorts
the function table and always binary-searches.

This commit drops the now-removed third argument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

AbstractInternalModule: functionsOrdered=true invariant is trusted but not enforced; silent function-lookup failure if array is unsorted

3 participants