Skip to content

Add condition contributors to DataLoader - #5661

Draft
fractal3000 wants to merge 2 commits into
masterfrom
feature/dataloader-condition-contributors
Draft

fractal3000 wants to merge 2 commits into
masterfrom
feature/dataloader-condition-contributors

Conversation

@fractal3000

@fractal3000 fractal3000 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

A DataLoader has a single mutable condition slot that several independent parties compete for: the application (XML or setCondition), composing filters (GenericFilter, standalone GroupFilter — capture a baseline and replace the slot) and accumulating filters (standalone PropertyFilter/JpqlFilter, DataGridHeaderFilter — append their node into the slot and edit it in place). Any pair of writers is a potential conflict, and the symptoms group into three families: a contribution lost when another party rebuilds the slot, a contribution duplicated when a party captures another's output as its base, and a load by a stale composition. The identity heuristics and snapshot machinery around GenericFilter exist only to work around this.

One concrete example: a standalone quick-search PropertyFilter attached to a loader after a composing filter has already built the slot loses its contribution on the next rebuild (e.g. a configuration switch) — its field still shows the value, the query no longer contains it, and further edits mutate an orphaned node.

Change

Commit 1 — additive API, no behavior change. DataLoader gets a registry of condition contributors:

  • ConditionContributor — a functional interface returning the party's current condition (null = does not participate).
  • DataLoader.addConditionContributor(...)Subscription; the default implementation throws UnsupportedOperationException so custom DataLoader implementations fail loudly at registration, not silently at load.
  • DataLoader.getEffectiveCondition() — the composed read-only view (own condition AND contributions) for anyone who needs to see the whole picture; getCondition() keeps its exact meaning (the slot).
  • All four loader implementations compose the effective condition in createLoadContext(), so the pagination count query and the DISTINCT heuristic are covered automatically. Contributions and the base enter the composed tree as copies — the composition never holds live nodes of another party. With no contributors the very same condition instance is used as before (pinned by a test).

Commit 2 — opt-in migration of standalone filters. With jmix.ui.component.standalone-filter-contributes-condition = true a standalone filter component registers itself as a contributor instead of appending its node into the slot (DataGridHeaderFilter, JpqlFilter and FullTextFilter inherit this through SingleFilterComponentBase); setConditionModificationDelegated(true) withdraws the contribution. The default is false and keeps the old behavior byte for byte, pinned by StandaloneFilterLegacyConditionTest. With the flag on, a standalone filter's contribution survives any rebuild of the slot by a composing filter — the quick-search scenario above becomes correct by construction, and so does the DataGridHeaderFilter case where Apply used to load by a base the application had already replaced.

This also makes application-level scenarios contractual that previously required mutating the shared slot and hoping: a tenant condition, a quick-search field over a grid, a session-attribute restriction — each is a registered contributor polled on every load.

Known problems this addresses

These map onto an inventory of the filter subsystem we maintain while working on #5516, #5623 and related issues. Most entries below have no public issue yet, so they are described here instead of linked.

Open defects - fixed when the property is on (none filed as issues so far):

  1. A standalone filter's condition is silently dropped when the application replaces the loader condition. A standalone PropertyFilter/JpqlFilter appends its node into the loader's condition tree once; after dataLoader.setCondition(...) the node is orphaned - the field keeps showing its value while the query no longer contains it. With a contributor the contribution is composed with the current slot on every load, whatever the application did to the slot.
  2. DataGridHeaderFilter applies by a base the application has already replaced (found while reviewing the One condition is shown but another is applied in GenericFilter #5516 fix in One condition is shown but another is applied in GenericFilter #5634): same mechanics through the header's internal PropertyFilter - the column shows an active filter, the query does not contain it.
  3. A filter attached to the loader after a composing filter built the slot loses its contribution on the next rebuild (e.g. a configuration switch) and freezes: the field shows a value, further edits mutate an orphaned node. Reproduced on a demo project before this change; correct by construction after it.

Open contract gaps - closed:

  1. Mixing a composing filter with standalone filters on one loader stops being an implementation accident. Today it works only because the composing filter's baseline copy is shallow and happens to share the standalone filter's live node; any change of that detail would break it silently. With contributors the split is contractual: the composing filter owns the slot, each standalone filter owns its contribution.
  2. Dynamic application conditions get a supported channel. A tenant condition, a quick-search field, a session-attribute restriction used to mean "mutate the shared tree in place and hope"; registering a contributor is now the supported way. (Application code that keeps mutating the tree in place is not auto-fixed - a composing filter can still rebuild the slot over it.)

Already-shipped fixes and documented limitations that stop being relevant (with the property on):

Deliberately not made obsolete: the composing filters' baseline capture, the identity heuristics and the re-navigation snapshot machinery (#5400, #5425, #5488) and the #5516 apply-path fix itself all remain necessary. They exist because a composing filter has to recognize its own output inside a shared mutable tree, and they go away only when GenericFilter/GroupFilter themselves become contributors - the major-version follow-up. After that step the "mine vs. foreign" question disappears: each party owns its contribution instead of recognizing it in the shared slot.

Out of scope

Composing filters (GenericFilter, GroupFilter) still own the slot the old way; migrating them (and dismantling the identity/snapshot machinery) is a separate, major-version conversation.

Testing

  • DataLoaderConditionContributorTest — the contributor contract on the loader level (composition, null contribution, per-load polling, copy semantics, unsubscription, ordering, identity without contributors).
  • StandaloneFilterConditionContributorTest — the flag on: the slot stays untouched, the contribution survives a slot rebuild by a composing filter, delegation withdraws it.
  • StandaloneFilterLegacyConditionTest — the flag off: the pre-existing behavior, pinned.
  • Full :flowui:test: 1263 tests, 0 failures.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants