Skip to content

feat: add spring-boot-morphium as optional module - #299

Merged
sboesebeck merged 13 commits into
sboesebeck:developfrom
Bardioc1977:pr/spring-boot-module
Aug 16, 2026
Merged

feat: add spring-boot-morphium as optional module#299
sboesebeck merged 13 commits into
sboesebeck:developfrom
Bardioc1977:pr/spring-boot-module

Conversation

@Bardioc1977

Copy link
Copy Markdown
Collaborator

Hi Stephan,

Third and final building block of the modularization, following #266 and #267: spring-boot-morphium, a Spring Boot auto-configuration for Morphium with the same Jakarta Data repository support as the Quarkus extension.

What this PR does

Three modules (morphium-spring-boot-autoconfigure, morphium-spring-boot-starter, morphium-spring-boot-test):

  • Auto-configuration — Morphium bean built from morphium.* properties (MorphiumProperties, MorphiumAutoConfiguration)
  • Jakarta Data repositories via JDK dynamic proxies (MorphiumRepositoryFactoryBean/MorphiumRepositoryRegistrar/MorphiumRepositoryInvocationHandler) — query derivation (findBy*/countBy*/existsBy*/deleteBy* with And/Or, Between, In, Like, ...), JDQL (@Query("WHERE status = :s ORDER BY name")), and @Find/@Delete with explicit @By field binding
  • @MorphiumTransactional as a registered auto-configuration (AOP-based commit/rollback via MorphiumTransactionAspect), not a plain @Component — that distinction mattered, see "Community review" below
  • Actuator health indicator (MorphiumHealthAutoConfiguration) surfacing connection status under /actuator/health
  • @MorphiumTest composite annotation (morphium-spring-boot-test) wiring InMemDriver for tests with no MongoDB needed
  • MorphiumRepository escape hatch for distinct()/query()/direct morphium() access when the derived-query surface isn't enough

Optionality

git diff --stat against develop shows only spring-boot-morphium, doc, and reactor-registration changes — morphium-core, poppydb, morphium-jakarta-data, and quarkus-morphium are untouched. Same pattern as #267: the spring-boot-dependencies BOM import stays in spring-boot-morphium/pom.xml, only the version property (spring-boot.version) lives in the parent, so a Spring Boot upgrade is a one-line change without every core build resolving ~250 Spring-managed coordinates.

Community review before this PR

Same process as #266/#267: a review PR against my own fork first (CodeRabbit auto-disabled on the non-default base branch, GitHub Copilot review triggered manually), which found three real bugs, all fixed and covered by regression tests before this PR:

  • MorphiumTransactionAspect was a plain @Component, not a registered auto-configuration — meant it silently never activated unless a consuming application happened to component-scan the auto-configure package itself, defeating the point of an auto-configuration starter
  • @By-bound parameters on @Find/@Delete methods were ignored — the binding info was parsed but never applied when building the query
  • CompletionStage-returning derived query methods dispatched synchronously on the calling thread instead of the configured async executor

Verification

  • Extension modules in isolation: 15/15 tests green
  • Full reactor (-Pextensions install): BUILD SUCCESS across all 13 modules
  • Full core test suite: no regression

What's next

With all three optional modules (morphium-jakarta-data, quarkus-morphium, spring-boot-morphium) now proposed, M6 (consolidation — CI workflow, release.sh cross-module check, doc pass) is the remaining piece of the original modularization plan.

Open questions for you

Adopt the spring-boot-morphium repository (branch move-to-morphium) as an
optional extension module, following the same pattern as
morphium-jakarta-data (M2) and quarkus-morphium (M4).

Copied per the M5-T4 dry-run copy-list (Abschnitt 1): root pom.xml, the
three publishable modules (morphium-spring-boot-autoconfigure/-starter/-test,
each pom.xml + src/), README.md, CHANGELOG.md and
docs-for-morphium/spring-boot.md. Repo-wide policy/CI files (.git, target/,
MIGRATION-NOTES.md, LICENSE, CODE_OF_CONDUCT.md, CONTRIBUTING.md,
SECURITY.md, .github/, .gitignore, .DS_Store) are intentionally not carried
over.

The module-local spring-boot.version property is removed from
spring-boot-morphium/pom.xml since it now inherits from morphium-parent
(see next commit); the spring-boot-dependencies BOM import stays in the
module POM per invariant I4.
Register the spring-boot-morphium module in the extensions profile,
after morphium-jakarta-data and quarkus-morphium (in that order),
plus a spring-boot.version property (3.4.13), analogous to the existing
quarkus.version pattern.

The spring-boot-dependencies BOM import stays in spring-boot-morphium/pom.xml
(invariant I4) -- only the version property moves here so a Spring Boot
upgrade is a single-line change in morphium-parent.
- docs/spring-boot.md: copied from spring-boot-morphium/docs-for-morphium/spring-boot.md
- mkdocs.yml: register the Spring Boot page in the Extensions nav section
  (Jakarta Data, Quarkus Extension, Spring Boot), replacing the M5 placeholder
  comment left by the quarkus-morphium wave
- docs/index.md: add a Spring Boot entry to the Extensions (Optional Modules)
  section, in the style of the existing Jakarta Data / Quarkus Extension entries
- CHANGELOG.md: entry for spring-boot-morphium under Unreleased/Added, after
  the quarkus-morphium entry, covering the three published artifacts, feature
  set, the two pre-integration naming corrections (module rename and property
  prefix rename), lockstep versioning with migration guidance, core
  independence via -DskipExtensions, no Docker requirement, and provenance
  from Bardioc1977/spring-boot-morphium

README.md/README.de.md intentionally left unchanged: neither carries a module
overview listing morphium-jakarta-data or quarkus-morphium either (verified
against the M2/M4 commits), so there is no existing structure to extend.
Registers morphium-spring-boot-autoconfigure, morphium-spring-boot-starter,
and morphium-spring-boot-test through the module registry (MODULE_DIRS/
MODULE_ARTIFACT_IDS/MODULE_EXTRA_CLASSIFIERS), and adds
morphium-spring-boot-parent as its own POM-only special case at both the
dry-run and real-release bundle-building sites, mirroring
morphium-parent/quarkus-morphium-parent.

Unlike quarkus-morphium/integration-tests, morphium-spring-boot-test IS meant
for publication: it is a user-facing test-support helper (the @MorphiumTest
composite annotation), analogous to poppydb as its own published artifact,
not an internal test suite -- this is a deliberate decision, not an
assumption carried over from the quarkus-morphium precedent.

Also extends ALL_POM_FILES with spring-boot-morphium/pom.xml, same reasoning
as the quarkus-morphium/pom.xml entry added in M4: mvn versions:set bumps
every pom.xml in the reactor regardless of registry membership.

Fixes a real packaging gap found while verifying: morphium-spring-boot-starter
has no source files at all (by design -- an empty jar that only pulls in
morphium-spring-boot-autoconfigure via a single Maven coordinate, following
Spring Boot's own starter convention). With a completely empty src/main/java,
maven-source-plugin and maven-javadoc-plugin silently produced no
-sources.jar/-javadoc.jar at all -- confirmed real Spring Boot starters (e.g.
spring-boot-starter-web) on Maven Central DO publish both, so this needed a
fix, not acceptance. Added a minimal package-info.java so both plugins have a
compilation unit to process; verified all three modules now produce
jar+sources+javadoc after the fix, and the full reactor build/tests remain
green.
…ation, not a plain @component

A plain @component in this library's own package is only picked up by
Spring Boot's component scan when the scan happens to cover that package --
for any real application depending on morphium-spring-boot-starter as an
external jar, component scan starts in the application's own base package
and never reaches here, so the aspect bean was never created and
@MorphiumTransactional methods ran without startTransaction()/commit/abort
at all, silently.

Changed to @autoConfiguration and registered in
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
alongside MorphiumAutoConfiguration and MorphiumHealthAutoConfiguration, so
Spring Boot's auto-configuration import mechanism instantiates it regardless
of the application's package structure.

Adds MorphiumTransactionAspectTest verifying the aspect bean actually exists
in the context and that @MorphiumTransactional methods commit on normal
return / abort on exception.

Found in code review on PR #18 (Bardioc1977/morphium).
…ived queries asynchronously

Two related bugs in MorphiumRepositoryInvocationHandler:

1. buildConditionsSpec() (used by @Find/@delete methods) read @PARAM instead
   of Jakarta Data's @by for field binding. Without -parameters (the parent
   compiler config does not set it), the fallback to the reflected parameter
   name produced conditions like "arg0:0" instead of the intended field name;
   even with parameter names available, an explicit @by value was ignored
   entirely. FindMethodBridge then queried the wrong/non-existent field, so
   annotated @Find/@delete methods returned no matches or affected the wrong
   data. Now reads @by, matching quarkus-morphium's MorphiumDataProcessor.

2. Derived query methods declared with a CompletionStage return type (e.g.
   findByStatusAsync) were excluded from returnsSingle but never dispatched
   to QueryMethodBridge.executeQueryAsync -- they always ran the synchronous
   executeQuery, so the generated proxy tried to cast the raw result (List,
   Long, etc.) to CompletionStage and threw ClassCastException instead of
   running asynchronously. Also strips the "Async" method-name suffix before
   parsing (e.g. "findByStatusAsync" -> "findByStatus"), matching
   quarkus-morphium's MorphiumDataProcessor convention -- without stripping
   it, MethodNameParser misreads the suffix as part of the field name and the
   query matches nothing.

Adds regression tests for both: an @find+@by method against real InMemDriver
data, and a CompletionStage-returning derived query resolved from a real
CompletableFuture.

Found in code review on PR #18 (Bardioc1977/morphium).
The M5 branch predates two version bumps on develop (6.3.0 -> 6.3.2).
morphium-parent's version moved on without these four POMs, which
still pointed at 6.3.0-SNAPSHOT -- Maven resolved the stale parent
model, so ${spring-boot.version} (defined only in the current
morphium-parent) was never interpolated and the BOM import failed
with a literal ${spring-boot.version} in the coordinate.

Also drops the explicit relativePath on the top-level module POM,
matching quarkus-morphium and morphium-jakarta-data, which rely on
plain reactor resolution instead.
@Bardioc1977
Bardioc1977 requested a balanced review from Copilot August 15, 2026 17:57
Bardioc1977 pushed a commit to Bardioc1977/spring-boot-morphium that referenced this pull request Aug 15, 2026
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

On the second open question — I'll answer that one myself rather than leave it hanging: preview, matching quarkus-morphium (status: "preview" in its quarkus-extension.yaml). Same reasoning applies here: this had a fork-side review pass, not a production track record yet.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds spring-boot-morphium, the third and final optional integration module in the modularization series (following #266 morphium-jakarta-data and #267 quarkus-morphium). It provides a Spring Boot auto-configuration for Morphium with Jakarta Data repository support, delegating all query logic to the shared morphium-jakarta-data runtime and adding only the Spring-specific wiring. The core remains untouched and the module builds only under the extensions profile.

Changes:

  • Three new Maven modules (morphium-spring-boot-autoconfigure, -starter, -test): auto-configured Morphium bean from morphium.* properties, Jakarta Data repositories via JDK dynamic proxies, @MorphiumTransactional AOP aspect (registered as an auto-configuration), an Actuator health indicator, and a @MorphiumTest composite annotation.
  • Reactor/registration wiring: spring-boot.version property + module entry in root pom.xml, release.sh module-registry additions, and mkdocs.yml nav entry.
  • Documentation: module README, docs/spring-boot.md, docs/index.md pointer, and root/module changelog entries.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
.../MorphiumTransactionAspect.java AOP transaction advice registered as auto-configuration; missing explicit after= ordering vs its @ConditionalOnBean.
.../MorphiumAutoConfiguration.java Builds/connects the Morphium bean with retry, SSL, and entity pre-scan.
.../MorphiumHealthAutoConfiguration.java Actuator health indicator, correctly ordered after MorphiumAutoConfiguration.
.../MorphiumRepositoryRegistrar.java / FactoryBean.java / InvocationHandler.java Repository scanning + JDK-proxy dispatch to jakarta-data bridges (signatures verified).
.../MorphiumProperties.java @ConfigurationProperties for morphium.* keys.
.../EnableMorphiumRepositories.java, MorphiumTransactional.java Public annotations for repository scanning and transactions.
spring-boot-morphium/*/pom.xml, pom.xml Module POMs, BOM import, annotation-processor path, centralized spring-boot.version.
release.sh Adds the three modules + spring-boot parent to the release bundle (arrays consistent).
Test sources AutoConfig, proxy, and transaction regression tests using InMemDriver.
README.md, docs/spring-boot.md, docs-for-morphium/spring-boot.md, docs/index.md, mkdocs.yml, changelogs Documentation; contain a stale 6.3.0-SNAPSHOT version and a broken README link.
Suppressed comments (1)

docs/spring-boot.md:304

  • This link points to morphium-spring-boot-starter/README.md, but the module README actually lives at spring-boot-morphium/README.md (there is no README under morphium-spring-boot-starter/), so this link is broken. Also use blob rather than tree for a file path. The same broken link appears in spring-boot-morphium/docs-for-morphium/spring-boot.md:304.
[`morphium-spring-boot-starter/README.md`](https://github.com/sboesebeck/morphium/tree/develop/morphium-spring-boot-starter/README.md)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

* }</pre>
*/
@Aspect
@AutoConfiguration
Comment thread spring-boot-morphium/README.md Outdated
<dependency>
<groupId>de.caluga</groupId>
<artifactId>morphium-spring-boot-starter</artifactId>
<version>6.3.0-SNAPSHOT</version>
Comment thread spring-boot-morphium/README.md Outdated
</dependency>
```

In the Morphium reactor, `${project.version}` currently resolves to `6.3.0-SNAPSHOT`.
Comment thread docs/spring-boot.md Outdated
</dependency>
```

In the Morphium reactor, `${project.version}` currently resolves to `6.3.0-SNAPSHOT`.
Comment thread docs/spring-boot.md Outdated
`InMemDriver` is Morphium's in-memory MongoDB emulation — tests run against it with no
container and no external MongoDB, exactly like the core Morphium test suite.

## Abgrenzung zu Spring Data MongoDB
- MorphiumTransactionAspect: order after MorphiumAutoConfiguration,
  matching MorphiumHealthAutoConfiguration. @ConditionalOnBean is
  evaluated against beans registered so far, so without this it only
  worked by alphabetical-sort coincidence -- any rename could disable
  the aspect silently.
- Sync stale 6.3.0-SNAPSHOT to the actual reactor version (6.3.2) in
  README.md, docs/spring-boot.md, docs-for-morphium/spring-boot.md.
- Fix broken README cross-link (wrong module path, tree instead of
  blob) in the two doc copies.
- Translate a German section heading in the two published (English)
  doc pages, third occurrence not flagged by review but same drift.
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Copilot's four findings are all real, fixed in 4f3d11743:

  • MorphiumTransactionAspect now declares @AutoConfiguration(after = MorphiumAutoConfiguration.class), matching MorphiumHealthAutoConfiguration. It only worked before by alphabetical-sort coincidence — @ConditionalOnBean is evaluated against beans registered so far, so a rename of either class could have silently disabled @MorphiumTransactional everywhere.
  • Stale 6.3.0-SNAPSHOT synced to the actual reactor version (6.3.2-SNAPSHOT) in the README and both doc copies.
  • The broken README cross-link (wrong module path, tree instead of blob) fixed in both doc copies.
  • The German section heading translated in both published (English) doc pages — plus a third occurrence in docs-for-morphium/spring-boot.md that review didn't flag but had the same drift.

Full reactor build green, 15/15 module tests still green after the change.

@sboesebeck sboesebeck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed at 4f3d117, built and tested locally. Conceptually this is well put together and the separation from core is clean — git diff --stat confirms your optionality claim, and the @AutoConfiguration-instead-of-@Component reasoning on MorphiumTransactionAspect is exactly right.

The problem is coverage: the 15 existing tests are green, but they only exercise the paths the proxy actually implements. I added six regression tests for features the module's own README advertises, and all six fail.

Test evidence

Module suite is now 21 tests — the 15 existing ones stay green, the six new ones are red:

Tests run: 21, Failures: 1, Errors: 5, Skipped: 0

derivedPageReturnUsesPageRequest       ClassCastException: java.util.ArrayList cannot be cast to
                                       jakarta.data.page.Page
derivedSortParameterIsApplied          AssertionFailedError: expected: <2> but was: <1>
                                       (Sort argument silently ignored, insertion order returned)
annotatedDeleteReturnsCount            NullPointerException: Cannot invoke "java.lang.Long.longValue()"
                                       because the return value of InvocationHandler.invoke(...) is null
annotatedFindCanReturnCompletionStage  ClassCastException: TestEntity cannot be cast to
                                       java.util.concurrent.CompletionStage
jdqlCanReturnCompletionStage           ClassCastException: TestEntity cannot be cast to
                                       java.util.concurrent.CompletionStage
repositoryDefaultMethodExecutes        UnsupportedOperationException: Unsupported repository method:
                                       TestEntityRepository.countActiveViaDefaultMethod

The common thread on 1–3: the shared morphium-jakarta-data module already has the bridge overloads these paths need (QueryMethodBridge.executeQuery with the four param indices, FindMethodBridge.executeAnnotatedDeleteCounted, executeFindAsync, JdqlMethodBridge.executeJdqlAsync). The Spring handler simply never calls them, while the Quarkus generator does. So these should be small fixes, not new functionality.

Findings

Blocking (details inline):

  1. Derived queries ignore dynamic pagination/sortingbuildDerivedQueryHandler never looks for PageRequest/Sort/Order/Limit parameters and calls the simple bridge overload. Page<T> returns fail with ClassCastException, Sort is dropped. Contradicts the README feature table ("Pagination", "Sorting").
  2. @Delete with an int/long return is broken — always dispatches to the void bridge and returns null, so unboxing NPEs. Jakarta Data 1.0 explicitly allows the delete count here.
  3. Async only works for derived queries@Find and @Query methods returning CompletionStage run synchronously and are then cast wrongly.
  4. default methods on repository interfaces don't work — they reach the normal dispatcher and hit UnsupportedOperationException; the proxy needs a method.isDefault() branch using InvocationHandler.invokeDefault(...).

Non-blocking:

  1. The entity pre-scan almost certainly doubles ClassGraph work rather than saving it — see inline; I traced the core call sites and the second scan is unavoidable in any real app.
  2. The README version will rot again at the next releaserelease.sh only bumps the two top-level READMEs. This is a pre-existing, generic problem (quarkus-morphium/README.md is already stale at 6.3.0-SNAPSHOT), so I'd rather fix it in release.sh separately than ask you to patch it here.
  3. Nested @MorphiumTransactional semantics need deciding — currently the inner call throws and the outer aspect aborts the whole transaction. At minimum document it; better would be REQUIRED-style reuse.

My take: good foundation, preview is the right status, and the answer to your doc question is that docs/spring-boot.md (MkDocs) is enough — no separate Antora tree. Points 1–4 should be fixed and covered by tests before merge; 5–7 can follow.

The tests

Take these over as-is if they're useful. Two files, both under morphium-spring-boot-autoconfigure/src/test/java/de/caluga/morphium/spring/autoconfigure/.

TestEntityRepository.java — added methods (rest of the file unchanged)
    Page<TestEntity> findByStatus(String status, PageRequest pageRequest);

    List<TestEntity> findByStatus(String status, Sort<TestEntity> sort);

    @Delete
    long removeByStatus(@By("status") String status);

    @Find
    CompletionStage<List<TestEntity>> byStatusAsync(@By("status") String status);

    @Query("WHERE status = :status")
    CompletionStage<List<TestEntity>> queryByStatusAsync(@Param("status") String status);

    default long countActiveViaDefaultMethod() {
        return countByStatus("active");
    }

New imports: jakarta.data.Sort, jakarta.data.page.Page, jakarta.data.page.PageRequest,
jakarta.data.repository.Delete, jakarta.data.repository.Param, jakarta.data.repository.Query,
java.util.concurrent.CompletionStage.

MorphiumRepositoryProxyMissingCasesTest.java — new file
package de.caluga.morphium.spring.autoconfigure;

import de.caluga.morphium.Morphium;
import jakarta.data.Sort;
import jakarta.data.page.PageRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * Regression coverage for repository-proxy features advertised by the Spring Boot module.
 * These tests intentionally expose the currently missing dispatch paths in PR #299.
 */
@SpringBootTest(classes = TestApplication.class)
@ActiveProfiles("test")
class MorphiumRepositoryProxyMissingCasesTest {

    @Autowired
    TestEntityRepository repository;

    @Autowired
    Morphium morphium;

    @BeforeEach
    void cleanUp() {
        morphium.clearCollection(TestEntity.class);
    }

    @Test
    void derivedPageReturnUsesPageRequest() {
        repository.save(new TestEntity("a", "active", 1));
        repository.save(new TestEntity("b", "active", 2));

        assertEquals(1,
                repository.findByStatus("active", PageRequest.ofSize(1)).content().size());
    }

    @Test
    void derivedSortParameterIsApplied() {
        repository.save(new TestEntity("a", "active", 1));
        repository.save(new TestEntity("b", "active", 2));

        List<TestEntity> result = repository.findByStatus("active", Sort.desc("priority"));

        assertEquals(2, result.getFirst().getPriority());
    }

    @Test
    void annotatedDeleteReturnsCount() {
        repository.save(new TestEntity("a", "active", 1));
        repository.save(new TestEntity("b", "active", 2));

        assertEquals(2, repository.removeByStatus("active"));
    }

    @Test
    void annotatedFindCanReturnCompletionStage() throws Exception {
        repository.save(new TestEntity("a", "active", 1));

        assertEquals(1,
                repository.byStatusAsync("active").toCompletableFuture().get().size());
    }

    @Test
    void jdqlCanReturnCompletionStage() throws Exception {
        repository.save(new TestEntity("a", "active", 1));

        assertEquals(1,
                repository.queryByStatusAsync("active").toCompletableFuture().get().size());
    }

    @Test
    void repositoryDefaultMethodExecutes() {
        repository.save(new TestEntity("a", "active", 1));

        assertEquals(1, repository.countActiveViaDefaultMethod());
    }
}

CI

Worth flagging separately: the only status on this head is security/snyk: success — there are no check runs, so no test suite has actually run against this PR on GitHub. That's the M6 CI-workflow item from your plan, but it means the green checkmark currently says nothing about the code.

delegate, parseableName, args != null ? args : new Object[0],
returnsSingle, returnsOptional, returnsBoolean, returnsStream, orderBySpec);
}
return args -> QueryMethodBridge.executeQuery(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (1/4): derived queries ignore dynamic PageRequest/Sort/Order/Limit.

Unlike buildFindHandler and buildJdqlHandler right below, this method never calls findParamIndex(...) and always dispatches to the simple executeQuery overload. Two consequences, both covered by the new tests:

  • a Page<T>-returning method gets the plain List result back and blows up in the proxy:
    ClassCastException: java.util.ArrayList cannot be cast to jakarta.data.page.Page
  • a Sort<T> argument is silently dropped — findByStatus("active", Sort.desc("priority")) returns insertion order, no error

Both are listed as supported in this module's README feature table ("Pagination: Page<T>, PageRequest, CursoredPage<T>" and "Sorting: Sort<T>, Order<T> as method parameters").

The fix should be mechanical: QueryMethodBridge.executeQuery already has the overload taking sortParamIndex/orderParamIndex/pageRequestParamIndex/limitParamIndex (QueryMethodBridge.java:234) — it is what the Quarkus generator calls, and it short-circuits back to the simple overload itself when all four indices are -1, so you can dispatch to it unconditionally. Note that isSpecialParam already exists here for exactly this parameter set.

private MethodHandler buildDeleteHandler(Method method) {
String conditionsSpec = buildConditionsSpec(method);
return args -> {
FindMethodBridge.executeAnnotatedDelete(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (2/4): @Delete with an int/long return type is broken.

This always calls the void bridge and returns null, so a long removeByStatus(...) method fails at the proxy boundary with

NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of
"java.lang.reflect.InvocationHandler.invoke(Object, java.lang.reflect.Method, Object[])" is null

Jakarta Data 1.0 allows void, int and long for a @Delete method, where the numeric variants return the number of deleted entities. FindMethodBridge.executeAnnotatedDeleteCounted (FindMethodBridge.java:301) already provides that count and is currently unused from here — so this is a return-type switch on method.getReturnType(), picking the counted bridge for int/long/Integer/Long and keeping the current call for void.

boolean returnsStream = Stream.class.isAssignableFrom(method.getReturnType());
String orderBySpec = getOrderBySpec(method);

return args -> JdqlMethodBridge.executeJdql(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (3/4a): @Query methods returning CompletionStage execute synchronously and are then cast wrongly.

buildJdqlHandler has no CompletionStage branch, and isSingleReturn doesn't exclude it either — so a CompletionStage<List<TestEntity>> method is analysed as a single-entity query, executed on the calling thread, and the resulting entity is handed to the proxy as if it were the stage:

ClassCastException: TestEntity cannot be cast to java.util.concurrent.CompletionStage

JdqlMethodBridge.executeJdqlAsync (JdqlMethodBridge.java:848) exists for this and is never called from the Spring module.

Same issue in buildFindHandler below — see the separate comment there. Only buildDerivedQueryHandler handles async today, which makes the asymmetry easy to miss: the README documents "Async: CompletionStage<T> return type" without restricting it to derived queries.

boolean returnsCursoredPage = CursoredPage.class.isAssignableFrom(method.getReturnType());
boolean returnsStream = Stream.class.isAssignableFrom(method.getReturnType());

return args -> FindMethodBridge.executeFind(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (3/4b): same async gap for @Find.

Mirror of the buildJdqlHandler comment above: no CompletionStage branch, isSingleReturn doesn't exclude it, so @Find CompletionStage<List<TestEntity>> byStatusAsync(...) runs synchronously and then fails with

ClassCastException: TestEntity cannot be cast to java.util.concurrent.CompletionStage

FindMethodBridge.executeFindAsync (FindMethodBridge.java:347) is the counterpart that should be called here.

Worth fixing both in one pass, and while you're in here: isSingleReturn returning true for any unrecognised wrapper type is what turns each of these gaps into a confusing ClassCastException at the call site rather than a clear error. An explicit unsupported-return-type check would make the next such gap self-diagnosing.

return buildDerivedQueryHandler(method);
}

throw new UnsupportedOperationException(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking (4/4): default methods on repository interfaces end up here.

A default method is a perfectly ordinary part of a Jakarta Data repository interface (the usual way to compose derived queries into a small piece of domain logic), but it matches none of the branches above and therefore fails with

UnsupportedOperationException: Unsupported repository method: TestEntityRepository.countActiveViaDefaultMethod

analyzeMethod needs a method.isDefault() branch before the derived-query check, dispatching via InvocationHandler.invokeDefault(proxy, method, args) (JDK 16+, so fine on the Java 21 baseline). Note that invokeDefault needs the proxy instance, which analyzeMethod's MethodHandler signature doesn't currently carry — either pass it through or special-case default methods directly in invoke before the handlers.computeIfAbsent(...) lookup.

}
if (!typeIds.isEmpty()) {
AnnotationAndReflectionHelper.registerTypeIds(typeIds);
log.info("Pre-registered {} entity type IDs, Morphium will skip ClassGraph scan", typeIds.size());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking: this log line is not accurate, and the pre-scan most likely costs more than it saves.

AnnotationAndReflectionHelper.registerTypeIds(...) only short-circuits the type-ID initialisation in AnnotationAndReflectionHelper.init(). Everything else in core still goes through ClassGraphCache, which builds its own full ScanResult on first use:

call site annotation / query
Morphium.java:264 @Messaging implementations
Morphium.java:314 @Driver implementations
Morphium.java:3358 @Capped
Morphium.java:3404 @Entity (startup index check)
ObjectMapperImpl.java:163 @Entity
InMemoryDriver.java:4411 MongoCommand subclasses

The @Driver lookup in particular is unconditional for this module: MorphiumProperties.driverName defaults to "PooledDriver" and buildConfig always sets it (:159), so Morphium's constructor always takes the driverName != null branch and triggers the cached scan. That scan would have covered the type IDs too.

So the net effect in a real application is two classpath scans instead of one — this method's own uncached new ClassGraph().enableAnnotationInfo().scan(), plus core's cached one — and the "Morphium will skip ClassGraph scan" message is wrong.

Simplest fix is to drop preRegisterEntities() entirely and let the cached scan serve everyone. If you'd rather keep a pre-registration hook (for a future Spring AOT/native story), the right one is ClassGraphCache.preRegisterClassesWithAnnotation(...), which the Quarkus module uses — but note its own javadoc: it only covers the name-based lookup path, not getClassInfoWithAnnotation/getSubclassesOf, so it would not make this scan-free either.

@Around("@annotation(de.caluga.morphium.spring.autoconfigure.MorphiumTransactional) || " +
"@within(de.caluga.morphium.spring.autoconfigure.MorphiumTransactional)")
public Object aroundTransactional(ProceedingJoinPoint pjp) throws Throwable {
morphium.startTransaction();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking, but please decide and document: nested @MorphiumTransactional calls.

startTransaction() is called unconditionally, and all three drivers reject a second one while a transaction is open — DriverBase.java:626 (PooledDriver/SingleMongoConnectDriver) and InMemoryDriver.java:5069 both throw IllegalArgumentException.

So one @MorphiumTransactional service method calling another currently means: the inner advice throws, the exception propagates into the outer advice's catch, which aborts the outer transaction and rethrows. All work of the outer method is lost, and the message the developer sees (transaction in progress) doesn't point at the cause. Since Spring developers arrive with @Transactional's REQUIRED default in mind, this shape will be encountered.

Options, in rough order of preference:

  1. REQUIRED-style reuse — track nesting depth (thread-local counter, matching the drivers' thread-local transaction state) and let inner invocations join the outer transaction: commit only when the outermost returns, abort marks the whole thing rollback-only.
  2. Explicit propagation attribute on @MorphiumTransactional, defaulting to REQUIRED.
  3. At minimum: document that nesting is unsupported, and detect it here with a clear error instead of letting the driver's IllegalArgumentException take out the outer transaction.

Also worth a doc line while you're here: the aspect has no rollback-rules concept, so unlike Spring's @Transactional (which by default rolls back on unchecked exceptions only) it aborts on any Throwable. That's a defensible choice, just a surprising difference.

<dependency>
<groupId>de.caluga</groupId>
<artifactId>morphium-spring-boot-starter</artifactId>
<version>6.3.2-SNAPSHOT</version>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking, and not really yours to fix — flagging so it doesn't get lost.

6.3.2-SNAPSHOT is hardcoded here and again at line 282, but release.sh only bumps the version snippets in the two top-level READMEs (README.md, README.de.md — see release.sh:280). So this will silently go stale at the next release.

That's not hypothetical: quarkus-morphium/README.md already says 6.3.0-SNAPSHOT in the same two places, one release cycle after #267.

The right fix is in release.sh — extend the bump loop over the extension modules' READMEs, ideally driven by the existing MODULE_DIRS registry so a fourth module is a registry entry rather than another hardcoded path. I'll handle that separately; no change needed in this PR.

@sboesebeck

Copy link
Copy Markdown
Owner

Answers to your open questions — including the two from #266 and #267 that I left hanging, sorry about that.

Docker in CI (#267)

Rather not. The reason is a general one rather than anything about your PR: almost everywhere in this project we assume that PoppyDB stands in when a test would otherwise need a real MongoDB. That is what it exists for — same wire protocol, its own replica-set election, embeddable directly (new PoppyDB(port, host, ...)) without any external process. Adding a container runtime as a CI dependency would introduce a second answer to a question we have already answered, and the two would drift.

The good news is that your code is already built that way. quarkus-morphium/integration-tests runs on InMemDriver with quarkus.morphium.devservices.enabled=false, and the comment in application.properties says exactly this ("no MongoDB process or Docker required"). As far as I can tell, MorphiumTransactionalTest is the single class that actually wants Docker, and it already degrades to skipped through DockerAvailableCondition rather than failing. So "no Docker" costs us one skipped class, not a broken build.

If you want that class covered rather than skipped, PoppyDB is a real option and not just in theory: MongoCommandHandler implements commitTransaction/abortTransaction and the lsid/txnNumber session handling, so a replica-set PoppyDB gives you genuine multi-document transactions to test against. Pointing that test at an embedded PoppyDB replica set instead of Dev Services would turn the one skip into real coverage. Entirely optional, and definitely not a condition for merging anything.

To be clear about what this does not mean: keep Dev Services as a feature. Auto-starting a MongoDB container is exactly right for a developer's laptop, and the Dev UI card and the container-startup path are worth having. This is only about what CI is allowed to depend on.

CI workflow (#266) — yes, as its own PR

Yes please, as a separate follow-up, and I would now put it higher than "nice to have". Some context on why.

Full pre-merge runs for this project happen on a dedicated machine here rather than on GitHub Actions: a five-phase matrix (in-memory, MongoDB replica set, PoppyDB replica set, MongoDB single, PoppyDB single) that takes about 2.5 hours. I looked closely at what it actually covers while reviewing this PR, and the answer is uncomfortable: it builds the whole reactor with -DskipTests and then runs the morphium-core suite five times, once per backend. There is a pre-gate that runs the poppydb module's own tests, but it is invoked as mvn -pl poppydb test, so it stops there.

The consequence is that the tests in morphium-jakarta-data, quarkus-morphium and spring-boot-morphium — 63 classes between them — currently run in nobody's pipeline. They only run when someone types mvn install locally. That is precisely how the four dispatch gaps I filed on this PR survived: the code compiles perfectly, it just does the wrong thing at runtime, and nothing was executing it.

I will widen the local gate to cover the extension modules regardless, since that is a one-line change on my side and does not need to wait for anything. But a GitHub-side workflow is the part that gives you a signal on a PR before I ever look at it, and right now the only status on this PR is Snyk. Given the Docker answer above, that workflow can be plain mvn -pl <extension modules> test on a stock runner — no services, no containers.

release.sh cross-module check (M6)

One coordination note so we do not build the same thing twice. I just pushed a change to release.sh that adds bump_module_readme_snapshots() plus a MODULE_README_FILES registry: module READMEs pin the reactor's current SNAPSHOT version, and bump_readme_versions() could never have fixed them, because it substitutes the last release version, which by definition never appears in them. quarkus-morphium/README.md had been sitting at 6.3.0-SNAPSHOT and morphium-jakarta-data/README.md at 6.2.6-SNAPSHOT while the reactor was on 6.3.2-SNAPSHOT; both are corrected now.

That is a bump, not a check. If your M6 idea is a verification step that fails the release when modules are inconsistent, the two fit together fine — just build it on top of that registry rather than beside it, and add spring-boot-morphium/README.md to MODULE_README_FILES when this PR merges.

The two questions on this PR

Both answered in my review, repeating them here so they are not buried: docs/spring-boot.md (MkDocs) is enough, no separate Antora tree — and preview, same as the Quarkus one.

…ries

Three defects in the repository proxy's method dispatch, all of them
features the module's README already advertises.

Derived queries never looked up dynamic Sort/Order/PageRequest/Limit
parameters and always dispatched to the simple bridge overload. A
Page<T> method therefore got a plain List back (ClassCastException at
the proxy boundary) and a Sort<T> argument was silently dropped, giving
insertion order with no error at all. Both branches now resolve the four
parameter indices the same way buildJdqlHandler already did and call the
overload that takes them; it short-circuits back to the simple overload
itself when no dynamic parameter is present.

@delete always called the void bridge and returned null, so the int/long
return types Jakarta Data permits failed unboxing null at the proxy.
Numeric return types now use executeAnnotatedDeleteCounted and report
the number of deleted entities.

@query and @find had no CompletionStage branch, and isSingleReturn did
not exclude CompletionStage either, so an async method was analysed as a
single-entity query, ran on the caller's thread, and handed the entity
itself back where a stage was expected. Both now dispatch to the async
bridges, and isSingleReturn excludes CompletionStage so the stage
completes with the list the signature promises rather than one element.

Also dispatch default methods through InvocationHandler.invokeDefault.
A default method carries its own implementation and matched none of the
analysis branches, so it ended in "Unsupported repository method".
Handled in invoke() rather than analyzeMethod() because invokeDefault
needs the proxy instance, which the MethodHandler interface cannot
carry, and checked before the derived-query branch since a default
method is free to be named findBy*.
…cond

REQUIRED propagation, same semantics as quarkus-morphium's
MorphiumTransactionalInterceptor: when a transaction is already active on
this thread, the advice participates in it rather than opening its own,
and leaves commit/abort to the outermost advised call.

Without this, one @MorphiumTransactional method calling another lost
everything the outer method had done: all drivers reject a second
startTransaction() with IllegalArgumentException, that exception
propagated into the outer advice's catch, and the outer transaction was
aborted. Spring developers arrive with @transactional's REQUIRED default
in mind, so this shape is reached easily.

No nesting counter is needed - Morphium already tracks the active
transaction per thread, which is why the Quarkus interceptor tests
getTransaction() rather than counting depth. Retry, write-buffer and
CosmosDB handling from that interceptor are deliberately not copied
here; they are separate concerns.

Documented on the advice: the REQUIRED semantics, and that this aspect
has no rollback-rules concept and therefore aborts on any Throwable,
unlike Spring's @transactional which by default rolls back on unchecked
exceptions only.
preRegisterEntities() ran its own uncached ClassGraph scan and called
AnnotationAndReflectionHelper.registerTypeIds, logging that Morphium
would skip its own scan. That claim was wrong: registerTypeIds only
short-circuits type-ID initialisation, while every other lookup still
goes through ClassGraphCache, which builds a full ScanResult on first
use. The @driver lookup in particular is unconditional for this module,
because driverName always has a value and buildConfig always sets it.

Net effect in a real application was two classpath scans instead of one,
plus a misleading log line. Removing the method lets the cached scan
serve everyone, which it already did for type IDs too.

Verified the method had a single call site and no test asserted on it.
One regression test per finding, each verified to fail without its fix.
They assert the resulting value, not merely the absence of an exception,
because three of these defects had a silent-wrong-result variant:

- dynamic Sort: asserts the actual ordering of three differently
  prioritised entities, since dropping the argument produced insertion
  order without any error
- Page return type: asserts a Page comes back at all
- counted @delete: asserts the returned count AND that the rows are gone
- CompletionStage on @query and @find: asserts the stage completes with a
  List rather than a single entity - the shape that a half-fix (async
  branch without excluding CompletionStage from isSingleReturn) would
  still have got wrong
- default method: deliberately named countBy* so it also proves the
  isDefault() check wins over derived-query parsing
- nested transactions: the inner call goes through a second proxied bean
  rather than a self-invocation, so the aspect really runs twice; asserts
  both documents survive, and that an inner failure rolls back both

23 tests total, up from 15.
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

All four blocking findings plus both non-blocking ones are fixed in a676cc488, ab215e032, 767041fee and 0298f3f37. Every fix has a regression test that I verified fails without it — details below, since three of these had a silent-wrong-result variant that a naive fix would have papered over.

The four blocking ones

1. Dynamic Sort/Order/PageRequest/Limit on derived queries. Both branches now resolve the four indices the way buildJdqlHandler already did and call the overload that takes them. One correction to your pointer: the overload with the four indices is at QueryMethodBridge.java:86, not :234:234 is the one without them. Your substantive point held: it short-circuits back to the simple overload itself when all four are -1, so calling it unconditionally is safe.

2. @Delete with int/long. Numeric return types now go through executeAnnotatedDeleteCounted. Test asserts both the count and that the rows are actually gone.

3. CompletionStage on @Query/@Find. Both dispatch to the async bridges now — and isSingleReturn excludes CompletionStage. That second half matters more than it looks: with only the async branch added, the ClassCastException disappears but returnsSingle stays true, so the stage completes with one entity instead of the list the signature promises. No exception, wrong result. The tests assert the shape (assertInstanceOf(List.class, ...) plus the element count) specifically to catch that variant.

4. default methods. Handled in invoke() via InvocationHandler.invokeDefault, i.e. your option (a) — not preference but necessity: invokeDefault needs the proxy, and MethodHandler only carries args. Placed before the derived-query branch, and the test method is deliberately named countByStatusViaDefaultMethod so it also proves the isDefault() check wins over name parsing.

The two non-blocking ones

preRegisterEntities() removed. I re-derived your causal chain against the code before deleting: registerTypeIds only short-circuits type-ID initialisation, the @Driver lookup is unconditional here because driverName always has a value and buildConfig always sets it, so the cached scan runs regardless. Two scans, and the log line was wrong. Single call site, no test asserted on it.

Nested @MorphiumTransactional. Built as REQUIRED propagation copied from MorphiumTransactionalInterceptor:136-142 rather than picked from your three options — the instruction here was to follow quarkus-morphium. Your option 1 in effect, but without the nesting counter, since Morphium already tracks the active transaction per thread. Retry, write-buffer and CosmosDB handling from that interceptor deliberately not copied. Documented on the advice, including the rollback-rules difference you asked for: this aspect aborts on any Throwable, unlike Spring's default.

The nesting test routes the inner call through a second proxied bean rather than a self-invocation — a self-call would bypass the aspect entirely and prove nothing.

Verification

  • 23 tests, up from 15. Full reactor: BUILD SUCCESS across all 13 modules.
  • Mutation proof per fix, reverting each individually and confirming the specific test goes red with the predicted error:
    • default methods → expected: <2> but was: <0>
    • Sort dropped → expected: <[9, 5, 1]> but was: <[1, 9, 5]> (the silent one)
    • counted delete → NullPointerException: Cannot invoke "java.lang.Long.longValue()" ... is null
    • Page return → ClassCastException: java.util.ArrayList cannot be cast to jakarta.data.page.Page
    • async @Query/@FindNonUniqueResult: Query returned more than one result
    • nested transactions → IllegalArgumentException: transaction in progress
    • files restored byte-identically afterwards, confirmed by md5
  • Nothing outside spring-boot-morphium touched. git diff against develop for the other modules shows only your own d905d4373 README bump, which this branch simply predates.

One I did not do

The explicit unsupported-return-type check. Every legal Jakarta Data return type for @Find including E[] would have to be enumerated, and this module supports no array return type today — so a tightened check would either have to permit arrays it cannot actually handle, or start rejecting something silently. Either way that is a behaviour decision of its own rather than a fix to this PR, and getting it wrong regresses working code. Happy to do it as a follow-up if you want array support settled at the same time.

Also: agreed on the CI workflow, and thanks for digging into what the five-phase matrix actually covers — 63 extension test classes running in nobody's pipeline explains this batch of findings better than anything else could. I will open that as its own PR (plain mvn -pl <extension modules> test, no services, no containers, per your Docker answer). For release.sh I will build on MODULE_README_FILES rather than beside it, and add spring-boot-morphium/README.md to it once this merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated no new comments.

Suppressed comments (4)

spring-boot-morphium/morphium-spring-boot-autoconfigure/pom.xml:51

  • The comment justifies the classgraph dependency as an "Entity-Pre-Scan in MorphiumAutoConfiguration", but MorphiumAutoConfiguration (and the rest of this module's main sources) performs no such pre-scan — it only builds MorphiumConfig, connects with retry, and forces the replica-set flag. As written, classgraph appears to be an unused direct dependency and the justification is inaccurate. Please either remove the dependency (and the docs/CHANGELOG claims that reference the pre-scan), or restore the pre-scan implementation if it was intended.
    <!-- classgraph: Dependency-Deklaration bleibt bestehen (Entity-Pre-Scan in
         MorphiumAutoConfiguration), die Versionsverwaltung übernimmt künftig
         morphium-parent's dependencyManagement (siehe morphium/pom.xml) statt
         einer eigenen Version hier. -->
    <dependency>
      <groupId>io.github.classgraph</groupId>
      <artifactId>classgraph</artifactId>
    </dependency>

docs/spring-boot.md:22

  • This claims MorphiumAutoConfiguration performs "a best-effort classpath pre-scan for @Entity/@Embedded classes so Morphium can skip its own internal scan at startup", but no such pre-scan exists in the module's code (MorphiumAutoConfiguration only builds config, connects with retry, and forces the replica-set flag). The same inaccurate claim appears in the identical spring-boot-morphium/docs-for-morphium/spring-boot.md copy and in the root CHANGELOG.md. Please drop this clause (or implement the pre-scan).
  transient failures (linear backoff) and a best-effort classpath pre-scan for
  `@Entity`/`@Embedded` classes so Morphium can skip its own internal scan at startup.

spring-boot-morphium/docs-for-morphium/spring-boot.md:22

  • Same inaccurate claim as in docs/spring-boot.md: MorphiumAutoConfiguration does not implement a classpath pre-scan for @Entity/@Embedded classes. Please drop this clause (or implement the pre-scan) to keep the docs consistent with the code.
  transient failures (linear backoff) and a best-effort classpath pre-scan for
  `@Entity`/`@Embedded` classes so Morphium can skip its own internal scan at startup.

CHANGELOG.md:509

  • This changelog entry describes "a best-effort classpath pre-scan for @Entity/@Embedded classes", but the module implements no such pre-scan (MorphiumAutoConfiguration only builds config, connects with retry, and adjusts the replica-set flag). Remove this claim so the changelog matches the shipped behavior.
IDE autocompletion), connection retry with linear backoff on transient failures, and a
best-effort classpath pre-scan for `@Entity`/`@Embedded` classes. Jakarta Data `@Repository`

Copilot's second review pass caught what removing preRegisterEntities()
in 767041f missed: an unused classgraph dependency declaration still
justified by that method, and three prose copies (docs/spring-boot.md,
its docs-for-morphium duplicate, CHANGELOG.md) still describing the
pre-scan as a shipped feature.

classgraph itself is not needed here - it already arrives transitively
through morphium (morphium-core declares it, morphium-parent centralises
the version), same as before the explicit declaration was added for the
now-removed method.
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Copilot's second pass caught what removing preRegisterEntities() missed: an unused classgraph dependency declaration still justified by that method, and the pre-scan claim left in three prose copies (docs/spring-boot.md, its docs-for-morphium duplicate, CHANGELOG.md). Fixed in b8cc572d2classgraph doesn't need an explicit declaration here, it already arrives transitively through morphium. Reactor build and the 23 module tests stay green.

@sboesebeck sboesebeck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

All six findings verified fixed. Merging as preview.

Correction on my side first: you are right about the bridge overload — the one taking the four indices is QueryMethodBridge.java:86, and :234 is the one without them. I checked; my review had it backwards. Thanks for not just following the pointer.

Your catch on finding 3 is the more valuable half of that fix, and I had missed it: adding only the async branch removes the ClassCastException but leaves returnsSingle true, so the stage completes with one entity instead of the list the signature promises. A green build and a silently wrong result. Asserting the shape rather than the absence of an exception is the right call.

What I checked, beyond taking the descriptions at face value:

  • Ran your 23 tests against the current develop core in a separate worktree — 23/23, BUILD SUCCESS. Your branch predates yesterday's commits there, so this confirms no incompatibility with today's core.
  • Footprint outside the module is only what an optional module needs: pom.xml, release.sh, docs/spring-boot.md, mkdocs.yml. morphium-core and poppydb untouched — the one-way dependency holds.
  • Every de.caluga import in the module's main sources is public API (Morphium, MorphiumConfig, config.CollectionCheckSettings, annotations.Id). No driver internals, nothing from driver.inmem.
  • Config uses the sub-object style throughout (driverSettings(), connectionSettings(), clusterSettings(), authSettings(), cacheSettings(), collectionCheckSettings()). The three remaining flat setters — setUseSSL, setSslContext, setAutoIndexAndCappedCreationOnWrite — are not deprecated and have no sub-object equivalent, so that is correct as written.
  • REQUIRED propagation goes through the public morphium.getTransaction() rather than reaching into driver state. Right call.
  • Dispatch delegates to the shared bridges everywhere now instead of reimplementing query logic, which was the substance of the original findings.
  • Your release.sh registry change merges cleanly with my MODULE_README_FILES commit — verified with git merge-tree, no conflict.

One documentation note, not a blocker. The REQUIRED propagation differs from Spring's in one respect worth a sentence in the docs: if an outer @MorphiumTransactional method catches an exception from an inner one, this aspect still commits, whereas Spring would have marked the transaction rollback-only. Deliberate given you did not copy the interceptor's other machinery, and fine for preview — just worth stating so nobody discovers it in production.

Reminder for the follow-up you already offered: add spring-boot-morphium/README.md to MODULE_README_FILES in release.sh.

On the CI workflow — one concrete finding from wiring the extension modules into our own test matrix last night, which will save you a debugging session. The first run that ever executed them here failed all 37 quarkus-morphium/integration-tests classes with QuarkusBindException: Port already bound: 8081. Not your code: 8081 is Quarkus's default test port and our dashboard's API happens to sit on it. I fixed it on our side with -Dquarkus.http.test-port=0. A GitHub runner will not have that specific collision, but the ITs binding a fixed well-known port is worth knowing about when you write the workflow. The other three modules were green: jakarta-data 82 tests, quarkus runtime 63, quarkus deployment 27.

@sboesebeck
sboesebeck merged commit 9b81e8e into sboesebeck:develop Aug 16, 2026
1 check passed
sboesebeck pushed a commit that referenced this pull request Aug 17, 2026
#305)

Follow-up Stephan flagged when merging #299: spring-boot-morphium/README.md
was missing from release.sh's MODULE_README_FILES, so it would have kept
pinning a stale SNAPSHOT version on every future release cycle -- exactly
what this array exists to prevent for the other two extension modules.

While verifying the fix actually bumps correctly (not just registering the
file and assuming it works), found two things the existing sed patterns in
bump_module_readme_snapshots() would not have matched even now:

- The "In the Morphium reactor, ... currently resolves to `X-SNAPSHOT`."
  sentence has "resolves to" between "currently" and the version, but the
  pattern is `currently [0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT` -- no wildcard for
  words in between. Reworded to "... is currently X-SNAPSHOT." (matching
  the working phrasing already used elsewhere) so the pattern actually
  fires.
- The prerequisites table's Morphium row still said "6.2.2" -- stale from
  before this module tracked the reactor version in lockstep, and in a
  format the "| Morphium | X-SNAPSHOT" pattern doesn't match either.
  Updated to the same "X-SNAPSHOT (built in lockstep as part of the
  reactor)" phrasing quarkus-morphium/README.md already uses successfully.

Verified end to end: copied the file, ran the exact three sed substitutions
bump_module_readme_snapshots() uses with a fake target version, confirmed
all four SNAPSHOT-bearing lines updated correctly, restored the original
(byte-identical, confirmed via diff) before committing the real fix.

Co-authored-by: Heiko Kopp <extern.heiko.kopp1@porsche.de>
Bardioc1977 pushed a commit to Bardioc1977/morphium that referenced this pull request Aug 17, 2026
There was no automated build/test workflow for PRs -- only doc deployment
and wiki sync. Every review so far (including sboesebeck#299/sboesebeck#303-305) ran entirely
on manual, ad hoc verification. This adds one: full reactor build/test on
every push to develop/master and every PR, using the standard `mvn verify`
(no -DskipExtensions, since this is the only place the three optional
extension modules -- morphium-jakarta-data, quarkus-morphium,
spring-boot-morphium -- get exercised automatically at all).

-Dquarkus.http.test-port=0 per your note on sboesebeck#299: quarkus-morphium's
integration-tests module binds Quarkus's fixed default test port (8081)
in its @QuarkusTest runs. 0 tells Quarkus to pick a free ephemeral port
instead, so a GitHub-hosted runner (or any other environment with
something else already on 8081) can't collide with it. Harmless for the
other modules -- only Quarkus reads the property.

Also fixed a stale doc comment in the parent pom.xml's -DskipExtensions
explanation: it still said "BOTH extension modules ... morphium-jakarta-data
AND quarkus-morphium", predating spring-boot-morphium's addition in sboesebeck#299.
Updated to name all three.

This PR's first run surfaced 3 messaging test failures that turned out to
be a test-base config bug (MultiDriverTestBase carrying inMemorySharedDatabases
=false along its InMemoryDriver fallback), not a morphium-core bug and not
something wrong with this workflow -- fixed separately in sboesebeck#300 (already on
develop) and confirmed green on rebase. See the PR discussion on sboesebeck#307 for
the full trail (two-environment reproduction, bisect, then root cause).
sboesebeck pushed a commit that referenced this pull request Aug 17, 2026
There was no automated build/test workflow for PRs -- only doc deployment
and wiki sync. Every review so far (including #299/#303-305) ran entirely
on manual, ad hoc verification. This adds one: full reactor build/test on
every push to develop/master and every PR, using the standard `mvn verify`
(no -DskipExtensions, since this is the only place the three optional
extension modules -- morphium-jakarta-data, quarkus-morphium,
spring-boot-morphium -- get exercised automatically at all).

-Dquarkus.http.test-port=0 per your note on #299: quarkus-morphium's
integration-tests module binds Quarkus's fixed default test port (8081)
in its @QuarkusTest runs. 0 tells Quarkus to pick a free ephemeral port
instead, so a GitHub-hosted runner (or any other environment with
something else already on 8081) can't collide with it. Harmless for the
other modules -- only Quarkus reads the property.

Also fixed a stale doc comment in the parent pom.xml's -DskipExtensions
explanation: it still said "BOTH extension modules ... morphium-jakarta-data
AND quarkus-morphium", predating spring-boot-morphium's addition in #299.
Updated to name all three.

This PR's first run surfaced 3 messaging test failures that turned out to
be a test-base config bug (MultiDriverTestBase carrying inMemorySharedDatabases
=false along its InMemoryDriver fallback), not a morphium-core bug and not
something wrong with this workflow -- fixed separately in #300 (already on
develop) and confirmed green on rebase. See the PR discussion on #307 for
the full trail (two-environment reproduction, bisect, then root cause).

Co-authored-by: Heiko Kopp <extern.heiko.kopp1@porsche.de>
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.

4 participants