Skip to content

feat: add quarkus-morphium extension as optional module - #267

Merged
sboesebeck merged 46 commits into
sboesebeck:developfrom
Bardioc1977:pr/quarkus-extension-module
Aug 7, 2026
Merged

feat: add quarkus-morphium extension as optional module#267
sboesebeck merged 46 commits into
sboesebeck:developfrom
Bardioc1977:pr/quarkus-extension-module

Conversation

@Bardioc1977

Copy link
Copy Markdown
Collaborator

Hi Stephan,

Glad #266 landed! Here comes the second building block of the modularization: quarkus-morphium, the Quarkus CDI extension for Morphium.

What this PR does

Four submodules (runtime, deployment, testing, integration-tests), three of them published:

  • CDI producer for Morphium, type-safe runtime configuration via @ConfigMapping (quarkus.morphium.*)
  • @MorphiumTransactional with CDI transaction events (MorphiumTransactionEvent), including automatic detection when Azure CosmosDB doesn't support multi-document transactions
  • Liveness/readiness/startup health checks via SmallRye Health
  • Dev Services (auto-started MongoDB container, optionally as a single-node replica set)
  • Dev UI card with live connection info
  • Build-time generated Jakarta Data @Repository implementations via Gizmo bytecode (no runtime reflection, no dynamic proxies)
  • GraalVM native-image support (automatic reflection registration for every @Entity/@Embedded class)
  • MorphiumId JSON serialization as its canonical 24-character hex string (Jackson + JSON-B)
  • MongoDB-backed migration runner with a distributed lock

groupId change

The extension previously published under io.quarkiverse.morphium, but it doesn't actually live in the Quarkiverse organization (no response from the maintainers on a namespace request). Maven coordinates now follow Morphium's own groupId, de.caluga:quarkus-morphium, with lockstep versioning to the reactor. Existing users of io.quarkiverse.morphium:quarkus-morphium:1.2.0 need to change the groupId to de.caluga and the version to the adopted Morphium version (currently 6.3.x) — no package renames, no API changes, only the Maven coordinates move.

Optionality

The core is unchanged (git diff --stat against develop confirms morphium-core, poppydb, morphium-jakarta-data are untouched). The core dependency tree is free of Quarkus/Testcontainers. -DskipExtensions builds only core + PoppyDB + morphium-jakarta-data. The Quarkus BOM import stays in the module POM (quarkus-morphium/pom.xml); only the version property (quarkus.version) lives centrally in the parent, so a Quarkus upgrade is a one-line change — if the BOM import itself moved to the parent, every core build would have to resolve ~400 Quarkus coordinates despite the core having no Quarkus dependency.

Docker and integration tests

integration-tests uses Testcontainers for real MongoDB instances. Without a running Docker daemon, the affected test class skips itself (via a DockerClientFactory.instance().isDockerAvailable() check), keeping the build green. The integration-tests module is not published to Central.

Extension conformance

A 30-point audit against the official Quarkus extension guidelines (naming scheme, build-step registration, deployment-artifact parity, quarkus-extension.yaml completeness) found 21 fully met, 5 partially met, and 3 violated points — all 3 violations were fixed before this PR (see "Community review" below).

Community review before this PR

As with #266, a review PR against my own fork ran first (CodeRabbit skipped it due to file count, GitHub Copilot found four real bugs): a missing build-step registration that would have silently disabled most extension features, a wrong native-image resource path (old groupId), Dev Services still starting a container despite an explicit InMemDriver, and a doc line showing a Jakarta Data 1.1 annotation that doesn't exist yet in the 1.0.0 version used here. All four fixed and covered by regression tests.

Verification

  • Extension modules in isolation: 40 tests green
  • integration-tests: 242/242 tests green (Docker via Testcontainers)
  • Full 9-module reactor: BUILD SUCCESS
  • Full core test suite: identical, already-known pre-existing flakes from feat: add morphium-jakarta-data as optional module #266 (messaging timing, Byte Buddy/JDK 25, no local Mongo server) — no regression

What's next

spring-boot-morphium as the third and final building block follows as a separate PR once this one is merged.

Open questions for you

  • Should the Antora docs (quarkus-morphium/docs/) be published anywhere, or is the new MkDocs overview page (docs/quarkus-extension.md) enough?
  • Extension status: preview or stable?
  • Is a Docker requirement for parts of CI acceptable from your side?

flgke81 added 7 commits August 6, 2026 07:26
Copies the quarkus-morphium Quarkus CDI extension (runtime, deployment,
testing, integration-tests submodules, plus Antora docs, README, and
CHANGELOG) from the standalone quarkus-morphium repository into this
reactor as a module directory, per the file selection assessed in
quarkus-morphium/MIGRATION-NOTES.md's "kommt mit" list.

Not yet wired into the reactor's module list -- that follows in the
next commit.
Adds the quarkus-morphium module to the "extensions" profile (see
D3-reactor-strategie.md, Variante B), positioned after
morphium-jakarta-data to reflect dependency order for readability
(Maven itself sorts the reactor regardless).

Also moves the quarkus.version property from quarkus-morphium/pom.xml
into morphium-parent (per D1, Absicherung B6) so a Quarkus upgrade is
a single-line change; the Quarkus BOM import itself stays in
quarkus-morphium/pom.xml (invariant I4) so core builds never resolve
Quarkus artifacts. Extends the parent's comment block to document this
distinction.
Registers quarkus-morphium (runtime), quarkus-morphium-deployment, and
quarkus-morphium-testing through the module registry (MODULE_DIRS/
MODULE_ARTIFACT_IDS/MODULE_EXTRA_CLASSIFIERS), and adds
quarkus-morphium-parent as its own POM-only special case at both the
dry-run and real-release bundle-building sites, mirroring morphium-parent
(add_module_to_bundle() always expects jar+sources+javadoc, which does not
apply to a packaging=pom module). integration-tests is a test-only submodule
with no publishing purpose and is simply not listed in the registry, so it
is never picked up.

Also extends ALL_POM_FILES with quarkus-morphium/pom.xml and
quarkus-morphium/integration-tests/pom.xml: mvn versions:set bumps every
pom.xml in the reactor regardless of whether it is registered as a
published module, so a reactor pom missing from ALL_POM_FILES would get
silently version-bumped by Maven but never staged by the git add calls in
this script, leaving it out of sync with the release commit.

Adds maven-source-plugin and maven-javadoc-plugin activation to
quarkus-morphium/{runtime,deployment,testing}/pom.xml — the parent POM
only declares them in pluginManagement, each module must still enable them,
same as morphium-jakarta-data. Without this, the module jars would build
but produce no sources/javadoc artifacts, which Sonatype rejects on upload.
…ative-image groupId path

Only MorphiumProcessor was listed in META-INF/quarkus-build-steps.list, the
mechanism Quarkus actually uses to load deployment build-step classes
(ServiceUtil.classesNamedIn(...)). MorphiumDataProcessor (Jakarta Data
repository generation), MorphiumDevServicesProcessor, MorphiumMigrationProcessor,
and MorphiumDevUIProcessor were never loaded, silently disabling most of the
extension's advertised features. All five processors are now listed.

Also fixes META-INF/native-image/io.quarkiverse.morphium/quarkus-morphium/
to META-INF/native-image/de.caluga/quarkus-morphium/ (GraalVM only picks up
native-image.properties when the directory matches <groupId>/<artifactId>,
and the groupId moved to de.caluga in the M3/M4 migration).

Additionally, Dev Services now skips container startup when
quarkus.morphium.driver-name is explicitly set to a non-production driver
(e.g. InMemDriver): previously only quarkus.morphium.hosts was checked,
so a Dockerless InMemDriver test setup still started a MongoDB container
before the runtime producer could apply the driver override. Verified:
integration-tests now starts exactly one container (for the one test that
actually needs a real replica set), down from one per test run before.

Reported by GitHub Copilot review on PR #17 (Bardioc1977/morphium).
… 1.1, not 1.0.0

The README and Antora jakarta-data page showed @by("price") @is(GreaterThanEqual)
as a supported example, but jakarta.data.repository.Is does not exist in
jakarta.data-api 1.0.0 (the stable version this module targets) — it was only
added in Jakarta Data 1.1, which has not shipped a final release yet (latest
available artifact is the 1.1.0-M3 milestone). The example would not compile
against this module's actual dependency. Corrected the example to a plain
equality @by parameter and added a note pointing to query derivation or
@query (JDQL) for non-equality conditions today, with @is noted as a natural
follow-up once Jakarta Data 1.1 finalizes.

Reported by GitHub Copilot review on PR #17 (Bardioc1977/morphium).

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.

🟡 Changes recommended

There are verified doc/config correctness issues (Antora config typo and misleading health-check documentation) that should be fixed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR adds quarkus-morphium as a new optional Quarkus CDI extension module within the Morphium reactor, keeping core artifacts extension-free while providing Quarkus integration features (config mapping, Dev Services, health checks, Dev UI, Jakarta Data repository generation, native-image support).

Changes:

  • Adds the new multi-submodule quarkus-morphium extension (runtime/deployment/testing + non-published integration-tests) with Quarkus runtime features, build steps, and extensive integration tests.
  • Updates the reactor/release tooling to publish the new Quarkus extension artifacts and include the intermediate quarkus-morphium-parent POM in the Central bundle.
  • Extends project documentation (MkDocs + Antora) and changelog to describe the new optional module and its usage.
File summaries
File Description
release.sh Adds Quarkus extension artifacts + parent POM to release bundle and staged POM list.
quarkus-morphium/testing/src/main/java/de/caluga/morphium/quarkus/testing/InMemMorphiumTestProfile.java Provides a Quarkus test profile for running against Morphium InMemDriver without Dev Services.
quarkus-morphium/testing/pom.xml Adds a published testing utility artifact for the extension.
quarkus-morphium/runtime/src/test/java/de/caluga/morphium/quarkus/json/MorphiumIdJsonbAdapterTest.java Unit tests for JSON-B MorphiumId adapter wire format.
quarkus-morphium/runtime/src/test/java/de/caluga/morphium/quarkus/json/MorphiumIdJacksonModuleTest.java Unit tests for Jackson MorphiumId module wire format.
quarkus-morphium/runtime/src/test/java/de/caluga/morphium/quarkus/health/MorphiumStartupCheckTest.java Unit tests for startup health check “everConnected” behavior.
quarkus-morphium/runtime/src/main/resources/META-INF/quarkus-extension.yaml Declares extension metadata (name, guide, keywords, config root).
quarkus-morphium/runtime/src/main/resources/META-INF/native-image/de.caluga/quarkus-morphium/native-image.properties Adds native-image build JVM args for JOL/module exports.
quarkus-morphium/runtime/src/main/resources-filtered/META-INF/morphium-version.properties Adds filtered build-time version properties exposed at runtime.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTxPhase.java Adds CDI qualifier for observing transaction lifecycle events.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTransactionEvent.java Adds CDI event type for transaction lifecycle phases and rollback failure.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTransactional.java Adds interceptor binding for Morphium transactions.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/SslConfig.java Adds TLS/X.509 nested runtime config mapping interface.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/MorphiumVersion.java Adds runtime helper to read extension/core/Jakarta Data versions.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/MorphiumDevUIJsonRpcService.java Adds Dev UI JsonRPC service exposing sanitized connection info.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/RollbackExecution.java Adds rollback method marker for migrations.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/MorphiumMigrationLock.java Adds distributed lock entity for migration execution.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/MorphiumMigrationEntry.java Adds changelog entity to track executed/rolled back/failed migrations.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/MorphiumMigrationConfig.java Adds migration runtime configuration (autostart, collections, TTL).
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/MorphiumChangeUnit.java Adds class-level marker for migration units with ordering metadata.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/Execution.java Adds execution method marker for migrations.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/LocalDateTimeConfig.java Adds config for LocalDateTime storage representation.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/json/MorphiumIdJsonbModule.java Registers JSON-B adapter when JSON-B capability is present.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/json/MorphiumIdJsonbAdapter.java Implements JSON-B MorphiumId ⇄ hex string mapping.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/json/MorphiumIdJacksonModule.java Registers Jackson serializer/deserializer for MorphiumId ⇄ hex string.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/health/MorphiumStartupCheck.java Adds startup probe with SRV-discovery-tolerant “everConnected” logic.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/health/MorphiumReadinessCheck.java Adds readiness probe with best-effort pool stats metadata.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/health/MorphiumLivenessCheck.java Adds liveness probe based on driver connection state.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/data/QuarkusMorphiumRepository.java Adds Quarkus base class for Gizmo-generated repositories with CDI-injected Morphium.
quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/CacheConfig.java Adds cache-related nested runtime config mapping interface.
quarkus-morphium/pom.xml Adds the intermediate extension parent POM with module list and BOM import.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/TransactionEventCollector.java Test bean collecting transaction events for assertions.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/TransactionalService.java Test service exercising @MorphiumTransactional.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/StatusStats.java Record DTO for aggregation test results.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/StatusCustomerCount.java Record DTO for aggregation test results.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/StatusCount.java Record DTO for aggregation test results.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/PaginatedOrderRepository.java Test Jakarta Data repository for keyset pagination.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/OrderEntity.java Test entity used for query/pagination/LocalDateTime scenarios.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumVersionTest.java Integration tests for @Version optimistic locking behavior.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumLocalDateTimeTest.java Integration tests for LocalDateTime mapping behavior.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumItemRepository.java Integration tests for MorphiumRepository features in Jakarta Data.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumInMemProfileTest.java Integration tests validating quarkus-morphium-testing profile behavior.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumInjectionTest.java Smoke tests for CDI-produced Morphium bean.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumIdResource.java Minimal REST resource for MorphiumId wire-format tests.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumIdJsonSerializationTest.java End-to-end REST test for MorphiumId (de)serialization.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumIdEntity.java Test entity with MorphiumId primary key.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumHealthCheckTest.java End-to-end tests validating health checks when enabled.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumHealthCheckDisabledTest.java End-to-end tests validating health checks absent when disabled.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumEntityRegistryTest.java Integration tests for build-time entity pre-registration.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumEmbeddedTest.java Integration tests for embedded document mapping.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDevServicesReplicaSetConfigTest.java Integration test ensuring replica-set override doesn’t break startup when Dev Services disabled.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDevServicesConfigTest.java Integration tests for Dev Services defaults/overrides in test profile.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataStreamTest.java Integration tests for Jakarta Data Stream support.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataProjectionTest.java Integration tests for JDQL SELECT projection mapping.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataParenGroupTest.java Integration tests for parenthesized condition parsing.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataPaginationTest.java Integration tests for pagination/sorting and derivation basics.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataMorphiumRepositoryTest.java Integration tests for MorphiumRepository escape hatches.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataHavingOrTest.java Integration tests for HAVING with OR in aggregates.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataGroupByTest.java Integration tests for GROUP BY record mapping and ordering.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataGroupByPageTest.java Integration tests for pagination with grouped results.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataDeleteTest.java Integration tests for delete derivation and deleteAll.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataCountFieldTest.java Integration tests for COUNT(field) NULL filtering behavior.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataAsyncTest.java Integration tests for CompletionStage async repository methods.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumDataAggregateTest.java Integration tests for aggregate functions (COUNT/SUM/AVG/MIN/MAX).
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumCrudTest.java End-to-end CRUD tests via injected Morphium + InMemDriver.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/ItemRepository.java Jakarta Data repository used by integration tests.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/ItemEntity.java Base entity shared across many integration tests.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/InitItemsMigration.java Test migration change unit exercising execution + rollback.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/FailingMigration.java Test migration change unit that forces rollback path.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/CustomerEntity.java Entity used for embedded/entity registry tests.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/AddressEmbedded.java Embedded type used for embedded/entity registry tests.
quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/AddCategoryMigration.java Second test migration change unit.
quarkus-morphium/integration-tests/src/main/resources/application.properties Integration-test runtime config (InMemDriver, Dev Services disabled).
quarkus-morphium/integration-tests/pom.xml Defines integration-tests module dependencies and Quarkus build plugin.
quarkus-morphium/docs/modules/ROOT/pages/index.adoc Adds Antora guide index page for the extension.
quarkus-morphium/docs/modules/ROOT/pages/includes/attributes.adoc Adds shared Antora attributes (versions/URLs).
quarkus-morphium/docs/modules/ROOT/pages/health-checks.adoc Adds health checks guide page.
quarkus-morphium/docs/modules/ROOT/pages/dev-services.adoc Adds Dev Services + Dev UI guide page.
quarkus-morphium/docs/modules/ROOT/nav.adoc Adds Antora navigation structure for extension docs.
quarkus-morphium/docs/antora.yml Adds Antora component descriptor for the extension docs.
quarkus-morphium/deployment/src/test/java/de/caluga/morphium/quarkus/deployment/MorphiumDevServicesProcessorTest.java Adds unit tests for Dev Services processor helper types/config equality.
quarkus-morphium/deployment/src/main/resources/META-INF/quarkus-build-steps.list Registers build step classes for the extension.
quarkus-morphium/deployment/src/main/resources/dev-ui/qwc-morphium-connection.js Adds Dev UI web component for showing connection info.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/RepositoryBuildItem.java Adds build item for discovered Jakarta Data repositories.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumMigrationProcessor.java Adds build-time discovery + runtime execution trigger for migrations.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumHealthBuildTimeConfig.java Adds build-time config mapping for enabling/disabling health checks.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumFeature.java Adds extension feature marker build item.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumEntitiesRegisteredBuildItem.java Adds ordering marker build item for entity registration step.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumDevUIProcessor.java Registers Dev UI card + JsonRPC provider in dev mode.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumDevServicesBuildTimeConfig.java Adds build-time config mapping for Dev Services settings.
quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MongoDBStartable.java Adds Testcontainers wrapper for starting MongoDB with optional replica set.
quarkus-morphium/deployment/pom.xml Defines deployment artifact dependencies (Quarkus build-time SPI, Testcontainers, Dev UI).
pom.xml Adds centralized quarkus.version property and activates quarkus-morphium in the extensions profile.
mkdocs.yml Adds Quarkus extension page to MkDocs navigation.
docs/index.md Links the Quarkus extension docs from the main docs index.
CHANGELOG.md Adds a changelog entry describing the new optional Quarkus extension module.
Review details
  • Files reviewed: 134/134 changed files
  • Comments generated: 4
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread quarkus-morphium/docs/antora.yml Outdated
- modules/ROOT/nav.adoc
asciidoc:
attributes:
page-toclevels: 3@
Comment on lines +9 to +10
NOTE: The extension already includes `quarkus-smallrye-health` as a transitive dependency.
No additional dependency is needed — health endpoints are available by default.
Comment on lines +40 to +41
* # Optional – overrides the subject DN extracted from the certificate:
* # morphium.ssl.x509-username=CN=myUser,O=myOrg,C=DE
Comment on lines +48 to +49
card.addLibraryVersion("de.caluga", "quarkus-morphium",
"Quarkus Morphium Extension", "https://github.com/Bardioc1977/quarkus-morphium");
Four issues found by GitHub Copilot's review on PR sboesebeck#267:

1. antora.yml: page-toclevels had a stray trailing '@' (3@ instead of 3),
   an invalid Antora AsciiDoc attribute value.
2. health-checks.adoc claimed quarkus-smallrye-health is a transitive
   dependency included by default; the runtime POM declares it optional
   (deliberately, so consumers who don't want smallrye-health aren't
   forced into it) -- corrected the NOTE to say it must be added explicitly.
3. SslConfig.java's Javadoc example used 'morphium.ssl.x509-username'
   instead of 'quarkus.morphium.ssl.x509-username' -- missing the
   'quarkus.' prefix this extension's config actually uses.
4. MorphiumDevUIProcessor's Dev UI card linked to the old, now-archived
   standalone repo (Bardioc1977/quarkus-morphium) instead of this
   extension's new home, sboesebeck/morphium.

Checked for the same bug classes elsewhere in the module: the
quarkus-morphium-showcase links are a different repo (correctly still
under Bardioc1977) and the other x509-username references already carry
the quarkus. prefix -- no further instances found.
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Thanks Copilot! All 4 findings confirmed and fixed:

  1. antora.yml: fixed the stray page-toclevels: 3@ typo.
  2. health-checks.adoc: corrected the note — quarkus-smallrye-health is deliberately optional, not transitive; added the explicit-dependency instruction.
  3. SslConfig.java: fixed the Javadoc example to use the actual quarkus.morphium.ssl.x509-username property key.
  4. MorphiumDevUIProcessor: updated the Dev UI card link from the old archived standalone repo to this one.

Also checked for the same bug classes elsewhere in the module (other x509-username references, other repo links) — no further instances found.

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.

🟡 Changes recommended

There are at least two correctness issues that can cause unintended behavior (Dev Services starting despite atlas-url configuration, and enum-state migration queries that may not match stored values).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/migration/MorphiumMigrationRunner.java:269

  • state is stored as the ChangeState enum (see recordExecution(...).setState(state)), but the query compares it to a string (EXECUTED.name()). In Morphium, enum queries are typically performed using the enum constant itself; using the string risks returning 0 executed migrations (and re-running already executed migrations) depending on enum mapping.
    quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MorphiumDevServicesProcessor.java:75
  • Dev Services are skipped only when quarkus.morphium.hosts is configured, but Morphium also supports quarkus.morphium.atlas-url as an alternative connection source. If an app sets atlas-url (and leaves hosts unset), this processor will still start a MongoDB container unnecessarily and inject host overrides that the runtime will ignore (since atlasUrl takes precedence).
        if (ConfigUtils.isPropertyNonEmpty("quarkus.morphium.hosts")) {
            log.debug("Morphium connection settings already configured – skipping Dev Services");
            return null;
        }
  • Files reviewed: 134/134 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

…is configured

MorphiumDevServicesProcessor only checked quarkus.morphium.hosts before
deciding whether to start a MongoDB container. Morphium also supports
quarkus.morphium.atlas-url as an alternative connection source (takes
precedence over hosts when set) -- an application configuring only
atlas-url would still get a Dev Services container started unnecessarily,
with host overrides injected that the runtime then ignores.

Found in code review on PR sboesebeck#267 (sboesebeck/morphium).
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Thanks for the second pass!

Dev Services + atlas-url: confirmed and fixed. Now also skips Dev Services when quarkus.morphium.atlas-url is set, matching the existing hosts check.

Enum-state migration query: verified, but this one is actually correct as-is — no change made. MorphiumMigrationRunner stores the ChangeState enum on a typed field (private ChangeState state), and Morphium's ObjectMapperImpl.serializeEnum() stores typed enum fields as the plain name() string (only untyped/Object-declared fields get the {class_name, name} wrapper). On the query side, MongoFieldImpl.checkValue() converts any Enum argument passed to .eq() via ((Enum) val).name() before building the filter — so .eq(EXECUTED.name()) and .eq(EXECUTED) produce byte-for-byte identical queries here. Happy to point at the exact lines if useful (ObjectMapperImpl.java serializeEnum(), MongoFieldImpl.java checkValue()).

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.

🟡 Changes recommended

The filtered morphium-version.properties currently references an undefined ${morphium.version} property, causing incorrect runtime version reporting/logging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

quarkus-morphium/runtime/src/main/resources-filtered/META-INF/morphium-version.properties:2

  • morphium.version is populated from ${morphium.version}, but there is no morphium.version Maven property in the reactor. With resource filtering this will typically end up as the literal string ${morphium.version} in the JAR, so MorphiumVersion.morphiumVersion() will report an incorrect value (and the startup log line in MorphiumProducer will be wrong). Since this module is lockstep-versioned with Morphium core, ${project.version} should be used here (or alternatively define a morphium.version property in the parent).
    quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/MorphiumRuntimeConfig.java:39
  • The example application.properties snippet contains quarkus.morphium.******, which is not a valid property key and reads like a placeholder that was meant to demonstrate setting the password. This makes the docs misleading for users copy/pasting the snippet.
  • Files reviewed: 134/134 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

flgke81 added 2 commits August 6, 2026 09:30
…ession test

morphium-version.properties referenced an undefined Maven property,
${morphium.version} -- no such property exists anywhere in the reactor.
With resource filtering this left the literal placeholder string in the
built JAR, so MorphiumVersion.morphiumVersion() (and the startup log line
in MorphiumProducer that uses it) silently reported the wrong value instead
of a real version.

Since this module is lockstep-versioned with Morphium core, uses
${project.version} -- the same expression already used for
extension.version in the same file.

Adds MorphiumVersionTest verifying none of the three version accessors
return "unknown" or a literal ${...} placeholder, and that
morphiumVersion() equals extensionVersion() (lockstep versioning).

Found in code review on PR sboesebeck#267 (sboesebeck/morphium).
quarkus.morphium.password=secret in the class-level Javadoc example reads
like an actual credential value rather than a placeholder -- replaced with
the conventional changeit placeholder used elsewhere in this codebase's
SSL/keystore examples.

Found in code review on PR sboesebeck#267 (sboesebeck/morphium).
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Thanks for the third pass!

morphium.version placeholder: confirmed and fixed. ${morphium.version} referenced a Maven property that doesn't exist anywhere in the reactor — with resource filtering that left the literal placeholder string in the built JAR instead of a real version. Since this module is lockstep-versioned with Morphium core, it now uses ${project.version}, the same expression extension.version in the same file already uses. Added MorphiumVersionTest to catch a regression here.

Example password in MorphiumRuntimeConfig Javadoc: confirmed and fixed. Replaced with the changeit placeholder already used elsewhere in this codebase's SSL/keystore examples, to avoid reading like a real credential.

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.

🟡 Changes recommended

The new Quarkus test modules depend on io.quarkus:quarkus-junit, which is likely the wrong artifactId (should be quarkus-junit5) and can break Maven resolution/builds.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumVersionTest.java:96

  • This test claims to cover an entity without @Version, but it uses ItemEntity, which is versioned. That makes the DisplayName/comments misleading and reduces the value of the test signal.
  • Files reviewed: 135/135 changed files
  • Comments generated: 2
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +28 to +31
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
</dependency>
Comment on lines +46 to +50
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
<scope>test</scope>
</dependency>

@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.

Hi Heiko,

first of all: impressive work again — clean module split, the Gizmo layer delegating to the shared morphium-jakarta-data bridges is exactly the right design, and the test discipline is well above average. Before this can go in, though, a deep review (Claude did the heavy lifting here, working through all 134 files — the findings below were verified against the actual code) turned up a number of issues that I'd like to see addressed.

This review is based on the current head including your Copilot-round fixes (c23ba60d10bc8700) — the ${morphium.version} placeholder and the atlas-url Dev Services skip are already resolved and not listed below. One note on Copilot's latest pass: the quarkus-junit finding is a false positiveio.quarkus:quarkus-junit exists on Central and is managed by quarkus-bom 3.32.3 (your green test runs confirm resolution works), so no change needed there.

Quick answers to your questions first:

  1. Antora docs: your call — I'd suggest publishing them under the existing gh-pages setup so they're reachable; the MkDocs overview page alone feels a bit thin for an extension this size.
  2. Extension status: let's stay with preview for the first release (which is what quarkus-extension.yaml currently says anyway) — given the findings below that's also the honest label. We can promote to stable in a follow-up release.
  3. Docker in CI: not a concern for me. I'm personally not a Docker fan, but if it works for you, it works for me — see finding 7 though, the no-Docker fallback currently doesn't do what the PR description claims.

Merge blockers

  1. quarkus.morphium.read-preference is silently ignored. MorphiumProducer.java:290 calls driverSettings().setDefaultReadPreferenceType(String) — a dead field in core that nothing consumes. The actual read path uses getDefaultReadPreference(), which defaults to ReadPreference.nearest(). So every app on a replica set reads with NEAREST instead of the documented default primary (stale reads out of the box), and no setting changes that. Fix: cfg.setDefaultReadPreferenceType(...) (which parses + sets) or build a ReadPreference and call setDefaultReadPreference(...).

  2. Gizmo generation produces broken classes for legal Jakarta Data signatures (MorphiumDataProcessor.java):

    • Non-void @Delete methods (long removeByStatus(...) — allowed by spec): the method descriptor uses the declared return type but both branches emit returnVoid()VerifyError at class load, i.e. app startup crash.
    • short/byte/char parameters: boxPrimitive/unboxPrimitive fall through the default branch → VerifyError as well.
    • Abstract methods the processor can't handle are silently skipped → AbstractMethodError on first production call. These should fail the build instead.
  3. Silently wrong query results:

    • @Find parameters without @By are dropped entirely — @Find List<Book> byAuthor(String author) returns all books. The @Query path already implements the parameter-name fallback (Jakarta Data §4.6.1, -parameters is enabled); the @Find path omits it.
    • Sort/Order/PageRequest/Limit on derived findBy* methods are ignored (wrong ordering), and Page returns run into a ClassCastException via returnsSingle.
  4. Transaction context can leak onto Quarkus worker-pool threads (MorphiumTransactionalInterceptor): catch (Exception) misses Errors, and a failing abort (primary stepdown — getPrimaryConnection throws before the clearTransactionContext() finally) leaves the ThreadLocal context set. The next request on that thread "joins" a dead transaction → silent data loss. Needs catch (Throwable) plus defensive context clearing. (Root cause of the abort case is arguably in morphium-core; happy to take that part there.)

  5. Commit retry can double-apply a transaction. A transient error thrown by the commit itself (e.g. code 251 after failover where the server actually committed but the reply was lost) retries the whole business method → all inserts applied twice. Drivers retry only the commit for exactly this reason, never the statements.

  6. Migration lock mutual exclusion breaks after the 60s TTL. The lock is never renewed during execution — a migration running longer than the TTL lets a second instance atomically steal the lock and run the same still-running change unit concurrently. A heartbeat extending expires_at (owner-guarded) is needed. Also: a held lock currently fails startup immediately instead of waiting — in a k8s rollout with 3 replicas, two pods crash-loop until the migration finishes. (The lock acquisition itself is genuinely TOCTOU-free — verified down through the writer/wire layers. Nice.)

  7. The advertised Docker-skip doesn't work. The assumeTrue(isDockerAvailable()) in MorphiumTransactionalTest sits in @BeforeAll, but QuarkusTestExtension boots the app (and Dev Services start the container — MorphiumDevServicesProcessor.java:103 has no Docker guard at all; compare Quarkus' own DevServicesMongoProcessor, which gates on DockerStatusBuildItem) before user @BeforeAll runs. Without Docker the build goes red instead of skipping. The check belongs in the processor (skip Dev Services with a warning) and/or a JUnit ExecutionCondition.

  8. @MorphiumTransactional on async/reactive methods commits too early. For Uni/CompletionStage returns (or the doXxxAsync repository methods, which run on morphium's async pool) the interceptor commits before the actual DB work happens on another thread. Should at least fail fast on async return types.

  9. Docs document a property that doesn't exist: quarkus.morphium.create-indexes (in configuration.adoc and README) — the real property is index-check. This also breaks the "all quarkus.morphium.* properties are unchanged" migration promise for 1.2.0 users. The configuration.adoc "documents every available property" claim misses 10 properties (including the whole migration.* group).

Should-fix (can be follow-up issues)

  • MorphiumBlockingCallDetector observes StartupEvent and dereferences the Morphium proxy → forced blocking connect (with the full retry ladder) on the boot thread; defeats lazy init and the startup health check. Rolling deploy with Mongo briefly unreachable → CrashLoopBackOff.
  • Liveness check is coupled to driver.isConnected() — a Mongo outage makes k8s restart every pod in a loop. DB connectivity belongs in readiness only (which already does it right, citing the Quarkus MongoDB extension precedent itself).
  • index-check=create-on-write-new-col leaves core's WARN_ON_STARTUP ClassGraph scan active → native-image crash; the one gap in the otherwise careful index-check matrix.
  • Dev Services skip logic is a blacklist (everything except PooledDriver skips) — SingleMongoConnectDriver users lose Dev Services. Should be an InMemDriver whitelist.
  • The default Dev Services path (replica-set container) has zero automated coverage — all integration tests run InMem with Dev Services disabled.
  • MorphiumStartupCheckTest tests a copy of the formula, not the production class — flipping || to && in MorphiumStartupCheck wouldn't fail any test.
  • Partial credentials (username without password) silently connect unauthenticated; malformed MorphiumId in a JSON body yields HTTP 500 instead of 400; (int) cast of cache.global-valid-time can overflow.
  • Empty-transaction tolerance depends on the exact server error string "Cannot start a transaction" (untested); BEFORE_COMMIT fires once per retry attempt (duplicate outbox side effects); CosmosDB detection is fail-open with a dead UnsupportedOperationException catch.
  • Migration polish: client-clock skew shortens the effective lock TTL; a failing rollback is only logged (not suppressed into the thrown exception, changelog stays FAILED); order compared as plain string ("10" < "2"); the idempotency requirement for change units is undocumented.
  • Native image: custom @Entity(nameProvider=...) classes aren't registered for reflection; subclasses of annotated base classes escape the Jandex scan (annotations aren't @Inherited, core resolves via hierarchy walk).
  • Docs drift: SNAPSHOT versions hardcoded in antora.yml/attributes.adoc/README (release.sh only touches poms); testing.adoc shows morphium.* without the quarkus. prefix; gaps/JAKARTA-DATA.md is stale.
  • pom.xml note: -DskipExtensions also skips morphium-jakarta-data (same profile), so it's "core + PoppyDB only" — the PR text says otherwise.

Verified as correct (so you don't re-check)

Core untouched (zero diff hunks in morphium-core), no Quarkus/Testcontainers in the core tree, BOM placement as described (3.32.3 resolves everything), all 5 @BuildStep classes registered, no io.quarkiverse coordinates left, integration-tests doubly excluded from publishing, release.sh sane (bash -n, module/artifact registries complete), Dev UI leaks no credentials (Atlas URL sanitized), all Gizmo call-site descriptors match the real bridge signatures, driver names match core's @Driver annotations, health checks are cheap per probe, and the STATIC_INIT/RUNTIME_INIT migration ordering via the marker build item is correct.

Happy to discuss any of these — and thanks again, the overall shape of this is very good.

flgke81 added 3 commits August 6, 2026 10:46
MorphiumProducer called cfg.driverSettings().setDefaultReadPreferenceType(String),
which sets a dead defaultReadPreferenceType field that nothing in morphium-core
reads. The actual read path uses DriverSettings.getDefaultReadPreference(),
which returns a separate ReadPreference-typed field defaulting to
ReadPreference.nearest() -- so every replica-set app read with NEAREST
instead of the documented default primary (stale reads out of the box),
and no configured value ever changed that.

Adds a small string-to-ReadPreference parser (no such parser existed in
core) and calls setDefaultReadPreference(ReadPreference) instead, with
regression tests for all five accepted values plus the case-insensitive
and unrecognized-value fallback paths.

Merge blocker #1 found by Stephan Boesebeck's review on PR sboesebeck#267
(sboesebeck/morphium).
…delete methods

Jakarta Data's @delete Javadoc requires: for a parameter-based (condition-only,
no entity argument) @delete method, the return type must be void, int, or long,
and if int/long, the method must return the number of deleted records.

executeAnnotatedDelete() always returned void, discarding the count entirely --
any int/long-returning @delete method that relied on it would necessarily be
wrong (or, in the Quarkus Gizmo generator, crash at class-load time -- fixed
separately in quarkus-morphium).

Adds executeAnnotatedDeleteCounted(), returning the number of deleted entities;
executeAnnotatedDelete() now delegates to it and discards the result, for
callers whose method is declared void.

Found in code review on PR sboesebeck#267 (sboesebeck/morphium) -- merge blocker #2
(shared with the quarkus-morphium Gizmo codegen fix).
…sitory codegen

Three related class-generation bugs in MorphiumDataProcessor, all found by
Stephan Boesebeck's review on PR sboesebeck#267 (merge blocker #2):

1. generateDeleteAnnotatedMethod() always emitted mc.returnVoid(), regardless
   of the method's declared return type. A legal Jakarta Data signature like
   `long removeByStatus(...)` (parameter-based @delete returning int/long,
   permitted -- see FindMethodBridge's companion fix) got a method descriptor
   promising a long/int return but bytecode that returns void: a VerifyError
   at class load, i.e. an application startup crash. Now dispatches to the
   new executeAnnotatedDeleteCounted() bridge and returns the count (boxed
   down to int via Math.toIntExact when needed) for int/long return types,
   keeping the void path for void-declared methods.

2. boxPrimitive()/unboxPrimitive() had no cases for SHORT/BYTE/CHAR parameter
   or return types -- they fell through to the `default -> value` branch,
   emitting no box/unbox instructions between an object type and a primitive
   slot. Also a VerifyError at class load for any repository method using
   these three types. Added all three cases to both methods, mirroring the
   existing DOUBLE/FLOAT/LONG/INT/BOOLEAN handling.

3. generateCustomQueryMethods() silently skipped any abstract repository
   method matching none of @Query/@Find/@Delete/@Insert/@Save/@update and no
   findBy*/countBy*/existsBy*/deleteBy* naming pattern, leaving it
   unimplemented on the generated class -- legal at class load, but any call
   throws AbstractMethodError on first production use. Now throws
   IllegalStateException at build time instead, naming the interface and
   method, so an unsupported repository method is caught during the build,
   not by a user hitting the endpoint.

Verified: full reactor build green, all 242 integration tests green (no
regression), including the existing @Delete/derived-query/Jakarta-Data-CRUD
suites that exercise this generator.
…ument ssl.tls-configuration-name

Two smaller items from the review.

isAsyncReturnType() only knew CompletionStage and io.smallrye.mutiny.Uni.
Multi has exactly the same problem: the interceptor commits right after
ctx.proceed() hands back the Multi, i.e. before the asynchronous work has
run, so it has to fail fast for the same reason Uni does.

While adding the test it turned out the pre-existing Uni detection was
never covered at all -- only CompletionStage/CompletableFuture were. Both
name comparisons are now tested. Since Mutiny is deliberately not a
dependency of this module, the test defines classes carrying those exact
fully-qualified names at runtime via ByteBuddy (already on the test
classpath transitively, no new dependency) and asserts the FQN before
asserting the detection, so the test cannot pass for the wrong reason. An
earlier attempt placed a stub source file in the io.smallrye.mutiny package
instead; that was dropped because it would collide with the real class as a
split package / duplicate class the moment Mutiny ever becomes a real
dependency.

quarkus.morphium.ssl.tls-configuration-name existed in SslConfig but was
missing from both configuration.adoc and the README table. A sweep over the
remaining ssl.* properties confirmed the other eight were already
documented.

Verified: 59/59 tests green in quarkus-morphium/runtime; configuration.adoc
table delimiters still balanced.

Remaining items from Stephan Boesebeck's re-review on PR sboesebeck#267
(sboesebeck/morphium).
@sboesebeck

sboesebeck commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Hi Heiko,

good news first: I've reviewed the A/B fix commits (0b5fab493, 89b40b71c, 55b21f018) — both fixes are correct and well-tested. The mutation-verified commit-retry test and the delete/count tests asserting actual database state are exactly the kind of coverage this needed. 👍

I'd like to get the release out, so here's the deal: you don't need to work through the whole remaining list before merge. Only two items left on the merge path, both one-liners:

  1. renewLock() must abort on n == 0. The update result already carries n (same as acquireLock() reads) — when the renewal matches 0 documents the lock was stolen, and continuing to run the remaining change units means two instances migrating concurrently. Throwing there turns the last remaining silent-corruption path into a loud failure. (The full in-flight heartbeat can wait — with this abort in place, the TTL-vs-longest-unit contract from the config Javadoc is an acceptable, documented limitation for preview.)

  2. CREATE_ON_WRITE_NEW_COL needs setIndexCheck(NO_CHECK). As per my earlier comment: the branch currently leaves core's default WARN_ON_STARTUP active (CollectionCheckSettings.java:9), and the Morphium constructor then runs the live ClassGraph scan (Morphium.java:575-577) — native-image crash for that mode. One line, same as the other switch branches.

Everything else moves to follow-up issues after the merge — none of it is silent-corruption class anymore:

  • Migration lock in-flight heartbeat + the wait test's lock _id (morphium_migration_lock vs migration_lock — it currently acquires a different document and never exercises the wait loop) + a real renewal test with a contender
  • Private interface methods failing the build (Jandex isDefault() is public && !static && !abstract — skip anything non-abstract instead); same for abstract toString()/equals()/hashCode() redeclarations; inherited abstract methods from super-interfaces escaping the check
  • One new, minor finding from the A-fix review: on SingleMongoConnectDriver a failed commit leaves the context set (it clears only after a successful execute()), so the retry's morphium.setTransaction(txContext) throws IllegalArgumentException("Transaction already in progress!") — retry never happens and the original error is masked. No data loss (the subsequent abort works), but guard it with if (attempt > 0 && morphium.getTransaction() == null).
  • The CosmosDB comment from 2cce8e29b documenting the UOE "safety net" (dead code, see my earlier comment) should be corrected whenever you touch that file next
  • try/catch + log around MorphiumBlockingCallDetector.registerListener(), a unit test for the new build-time Limit/PageRequest rejection, @Delete with boolean/Integer/Long returns → build-time error instead of VerifyError

Ship the two one-liners and I'll approve. If you want, collect the follow-up list into issues after the merge — happy to review those at whatever pace.

I want to include the modules into the 6.3.0 release. and because of some production issues, we need the fix rather earlier than later. Hence I'd like to release soon (today / tomorrow)

flgke81 added 2 commits August 6, 2026 16:38
… gate the Docker-dependent test before boot

The previous attempt only moved the detector's listener registration onto a
background thread. That kept the boot thread free but did not stop the
eager connect: registerListener() still called morphiumInstance.get(),
which dereferences the CDI proxy and runs MorphiumProducer.buildMorphium()
with its full retry ladder. Without Docker, Dev Services now skips
cleanly, the app boots against the default localhost:27017, and that
connect fails after retrying -- while MorphiumTransactionalTest's
@BeforeAll Docker check never gets a chance to run, because @QuarkusTest
boots the application before JUnit's @BeforeAll.

The detector no longer resolves Morphium at all. It is not a CDI bean
anymore (removed from AdditionalBeanBuildItem), the @observes StartupEvent
observer and its thread are gone, and it is now a static
registerOn(Morphium) called from buildMorphium() right after the connect
succeeded. That makes it structurally impossible for the detector to be the
cause of a connect, instead of merely unlikely.

Additionally, MorphiumTransactionalTest is now gated by a JUnit
ExecutionCondition that calls DockerClientFactory.instance()
.isDockerAvailable() directly -- JUnit evaluates conditions before
QuarkusTestExtension boots the application, which the replaced
@BeforeAll assumeTrue could not. Testcontainers'
@EnabledIfDockerAvailable is deliberately not used; its detector
misreports under Quarkus test classloading (already documented in that
class).

Verified with Docker: full reactor build green, MorphiumTransactionalTest
runs (5/5 green, 0 skipped -- i.e. the condition correctly does NOT
disable it when Docker is present), 59/59 runtime-module tests green.

NOT verified: an actually Docker-less run. Three simulation attempts all
failed for traceable reasons: DOCKER_HOST is ignored by Testcontainers'
UnixSocketClientProviderStrategy (hardcoded /var/run/docker.sock);
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE does make isDockerAvailable() return
false but Quarkus's own guard uses ContainerRuntimeUtil, i.e. the docker
binary, so DockerStatusBuildItem still reported available;
-Dquarkus-local-container-runtime=UNAVAILABLE is overridden by a pinned
strategy in ~/.testcontainers.properties. Worth noting independently: the
project relies on two different Docker detections (Quarkus binary check vs
Testcontainers socket check) that can disagree -- likely the same reason
@EnabledIfDockerAvailable misbehaved here. This needs verifying on a
genuinely Docker-less machine or in CI.

Blocker 7 follow-up from Stephan Boesebeck's re-review on PR sboesebeck#267
(sboesebeck/morphium).
… 2 review

1. Private interface methods broke the build -- a regression my own earlier
   fix introduced. Jandex's isDefault() means "public && !static &&
   !abstract", so a private interface method (legal since Java 9, it has a
   body) is not "default", fell through every guard, and hit the
   "no recognized pattern" IllegalStateException. Perfectly legal user code
   failed the build. The guard now skips everything non-abstract via
   isAbstract(), plus an explicit skip for abstract toString()/equals()/
   hashCode() redeclarations (Object provides those regardless).

2. A @delete method returning boolean/Integer/Long still produced the
   original VerifyError at class load: the returnsCount guard only covered
   primitive int/long, so anything else fell into the void branch and
   generated a bare return for a method whose descriptor promises a value.
   Now a build-time error with a clear message. Jakarta Data only permits
   void, int or long for a parameter-based @delete.

3. Abstract methods inherited from a CUSTOM super-interface escaped the
   build-time check entirely, because Jandex's methods() only returns
   declared methods -- so they still hit AbstractMethodError on first call.
   The scan now walks interfaceTypes() recursively, treating the four
   standard Jakarta Data interfaces as hierarchy dead-ends so their CRUD
   methods aren't misreported, with a visited-set guarding against diamond
   inheritance.

4. The @by parameter-name fallback (Jakarta Data 4.6.1) was added to the
   @find path but not to generateDeleteAnnotatedMethod, which still only
   looked for @by. A @delete method relying on parameter names got
   hasByParams=false, was treated as an entity-parameter delete, and called
   doDelete(stringArg) at runtime -- trying to delete a String as an entity.

New MorphiumDataProcessorCustomMethodsTest covers all four against a real
synthetic Jandex index (same approach as MorphiumProcessorReflectionTest):
a private helper plus an abstract toString() must NOT break generation; an
unsupported @delete return type must fail at build time; a method declared
only on a custom super-interface must still be generated (verified it is
declared on WithAudit only, not on the repository itself -- exactly the case
item 3 previously missed); and the @delete parameter-name fallback is
treated as a condition delete.

Verified: full reactor build green, 23/23 deployment-module tests green
(19 existing + 4 new).

Blocker 2 follow-up from Stephan Boesebeck's re-review on PR sboesebeck#267
(sboesebeck/morphium).
@sboesebeck

Copy link
Copy Markdown
Owner

Hi Heiko,

reviewed 43dbc2224 and 87b44e0dc. The detector redesign is the structurally right solution (static registerOn() after the connect — can't ever cause one; and it obsoletes my try/catch remark, no thread left to die), the ExecutionCondition is the correct gating mechanism, and codegen items 1–3 check out including the synthetic-index tests. But item 4 introduces a new silent regression, same failure class as the one it fixes:

Entity-parameter @Delete lifecycle methods are now silent no-ops. The parameter-name fallback in generateDeleteAnnotatedMethod marks every named parameter as a condition — and this reactor compiles with -parameters everywhere, so parameters always have names. The canonical Jakarta Data lifecycle pattern:

@Delete
void remove(CustomerEntity customer);

now takes the condition branch with conditionsSpec = "customer:0" instead of the entity branch (doDelete). Chain from there: executeAnnotatedDeleteCountedresolveMongoField(entityClass, "customer")getMongoFieldName throws → fallback returns the raw name (QueryExecutor.resolveMongoField, the catch returns javaFieldName) → query {customer: <entity object>} → matches nothing → query.delete() removes 0 documents and the method returns normally. Deletes nothing, looks like success. The else branch ("Single entity parameter → delegate to doDelete") is effectively unreachable now — it only triggers when parameter names are absent, which never happens in this build.

(Standard CrudRepository.delete(entity) is unaffected — that goes through the CRUD_METHODS path — it's specifically custom @Delete-annotated methods with an entity parameter. There's no integration test for that pattern, which is why the suite stayed green; the new deployment test only covers a String-typed condition param.)

Suggested fix: apply the name-fallback only to parameters whose type is not the entity type (or a List/collection of it) — an entity-typed parameter is a lifecycle delete by spec, regardless of its name. While you're there, generateDeleteAnnotatedMethod is the one generator that doesn't receive entityFields — passing it in would let you validate fallback names at build time exactly like the @Find path does, so a typo'd parameter name fails the build instead of silently matching nothing. Plus one integration test with an entity-parameter @Delete method.

And a gentle reminder since the comments may have crossed mid-work: the two one-liners from my 16:25 comment (renewLock() abort on n == 0, setIndexCheck(NO_CHECK) in the CREATE_ON_WRITE_NEW_COL branch) are still pending — those two plus this entity-delete fix are the complete remaining merge path. Everything else stays follow-up as agreed.

…ng change unit, and prove both

Follow-up on blocker 6. Three things were wrong beyond the original fix.

renewLock() ignored its own update result. When the owner-guarded update
matched 0 documents -- i.e. another instance had taken the lock over -- this
instance carried on and ran the remaining change units concurrently with the
new owner. It now inspects "n" the way acquireLock() already did and aborts
the run. The comment claiming subsequent writes would be no-ops anyway was
simply wrong and is corrected: only releaseLock() is owner-guarded,
recordExecution() and the change units themselves write unconditionally.

Renewal only happened BETWEEN change units, so a single unit running longer
than the TTL (an index build on a large collection, say) still allowed the
atomic steal of that very in-flight unit. Rather than documenting the
constraint away, there is now a real in-flight heartbeat: a daemon thread
renewing the lock while a unit is still executing, stopped in a finally
block, owner-guarded, with its failure surfaced instead of swallowed.

Writing the tests for that exposed a genuine bug in the heartbeat itself:
its tick interval was TTL/3 but floored at one second, so for any small
lockTtlSeconds the first tick fired only after the TTL had already elapsed
-- the heartbeat was structurally unable to renew in time. The floor is now
200ms, which keeps the "don't hammer the database" intent for realistic TTLs
while making small ones actually serviceable.

Both regression tests were rewritten, because the previous pair could not
work. The obvious "a contender calls acquireLock() and must fail" approach
is impossible against InMemDriver: an upsert whose filter matches nothing
(expires_at still in the future) is seeded from the equality predicates only
-- correct -- but then goes through storeInternal(), which treats an
existing _id as a replace rather than raising a duplicate key error the way
a real server would. So any contender steals a still-valid lock there,
regardless of renewal. That is also why the original test could never prove
anything. The tests now prove renewal positively instead: an observer thread
reads the lock document mid-run and asserts the owner is unchanged and
expires_at has moved forward. The driver divergence is documented at the
tests.

Both are mutation-proofed, which mattered: the first version of the
between-units test stayed GREEN with renewLock() disabled -- the same
mistake the reviewer had called out, repeated. At a 1s TTL the heartbeat
ticks every ~333ms, so with 800ms units it was silently renewing the lock
and masking the missing call. That test now uses a 6s TTL, making the
heartbeat interval (2s) longer than any single unit. Disabling renewLock()
now reddens only the between-units test, disabling the heartbeat only the
in-flight test, each on its own assertion, the other 8 staying green.

The wait test's lock id was wrong too (it seeded "morphium_migration_lock"
while the runner uses "migration_lock"), so the polling loop was never
exercised -- it would have stayed green with acquireLockWithWait() deleted.
It now goes through an accessor instead of a copy-pasted literal, so
renaming the constant cannot blind it again.

Verified: full reactor clean build green, 254/254 integration tests green
(0 failures, 0 errors, 0 skipped), Migration Framework 9/9.

Blocker 6 follow-up from Stephan Boesebeck's re-review on PR sboesebeck#267
(sboesebeck/morphium).
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Thanks for the deep re-review — the two regressions my own fix wave introduced were exactly the kind of thing I'd rather hear now than after a merge. Status per item:

A — commit retry was a no-op and converted data loss into reported success

Confirmed and fixed. You were right about the mechanism: PooledDriver.commitTransaction() clears the context in its finally block even when the commit command failed, so safeCommit() saw getTransaction() == null on the retry and returned as success. Code 251 got the right answer by accident; code 112 silently lost every write.

safeCommitWithRetry() now snapshots the context before the first attempt and re-installs it via morphium.setTransaction() before each retry, so the retry actually re-issues the commit. I left the core-side PooledDriver change to you as agreed.

Also added the test coverage that was missing entirely for this path. This needed Mockito (test scope, version from the inherited Quarkus BOM) — hand-faking MorphiumDriver for one method wasn't maintainable. Two tests: a fake driver that mimics PooledDriver exactly (clears the context in a finally even when throwing), first commit throws 112, second succeeds, asserting commitTransaction() is invoked twice; plus code 11000 asserting it is not retried and propagates.

Test quality verified by mutation, not by passing: commenting out setTransaction(txContext) turns the first test red with expected: 2 but was: 1 — exactly the silent-success path. Restored and re-verified green.

One note: Mockito's inline mock maker cannot instrument Morphium on JDK 25 (ByteBuddy retransform failure), so these tests need JDK 21. I installed it locally but did not commit a .tool-versions pin — that's your call, not mine.

B — deleteBy/countBy/existsBy with a dynamic parameter

Confirmed, including that it's a real regression (that signature did delete before, the argument was just ignored). Root cause exactly as you described: the new 12-arg overload builds its own query and never consults descriptor.prefix().

Non-FIND prefixes now delegate to QueryExecutor.execute(). A dynamic Sort/Order on a non-FIND prefix is accepted and ignored (no result list to reorder). Limit/PageRequest on countBy/existsBy/deleteBy is rejected at build time instead — "the 3rd page of a delete" has no sensible definition, and neither countAll() nor query.delete() has a skip/limit-bounded variant, so failing the build beats silently dropping a parameter someone wrote expecting it to work. If you'd rather have it silently ignored for compatibility, say so and I'll change it.

Tests: the delete test asserts the actual document count before (3) and after (1) plus that findByStatus("OPEN") is empty — not just a plausible return value. Count and exists assert the long/boolean that previously threw ClassCastException. Gap I'm aware of: the build-time rejection itself has no automated test yet; that needs a synthetic-Jandex-index unit test in the deployment module, since a deliberately failing build can't be asserted green from integration-tests.

Blocker 2 residuals — all four fixed

  • Private interface methods breaking the build (my own fix's fault — thanks for catching it, that's broken legal user code): guard now skips everything non-abstract via isAbstract() instead of isDefault(), plus an explicit skip for abstract toString/equals/hashCode redeclarations.
  • @Delete returning boolean/Integer/Long: now a build-time error with a clear message instead of a VerifyError at class load.
  • Methods inherited from a custom super-interface: the scan now walks interfaceTypes() recursively, with the four standard Jakarta Data interfaces treated as hierarchy dead-ends so their CRUD methods aren't misreported, and a visited-set against diamond inheritance.
  • @By fallback missing in generateDeleteAnnotatedMethod: same parameter-name fallback as the @Find path now applies.

19/19 deployment-module tests green. Unit tests for these four are still outstanding — flagging that rather than implying coverage.

Blocker 6 — migration lock

renewLock() now inspects n and aborts the run on n == 0, and I corrected that inaccurate comment about subsequent writes being no-ops.

The wait test's lock-id bug was real and embarrassing — it would have stayed green with acquireLockWithWait() deleted entirely (it seeded "morphium_migration_lock" while the runner uses "migration_lock", so the polling loop was never exercised). It now references the real constant through an accessor instead of a copy-pasted literal, so renaming the constant can't blind it again.

For the in-flight steal I went with the real heartbeat rather than documenting the constraint away: a daemon thread renews the lock while a single change unit is still running, terminated in a finally, owner-guarded, with the failure surfaced instead of swallowed. Docs updated accordingly (the TTL now only needs to exceed the heartbeat interval, not the runtime of any unit).

Writing the tests for this surfaced two things worth telling you about.

A real bug in the heartbeat itself: HEARTBEAT_TICKS_PER_TTL = 3 was capped by a hard one-second floor, so with a small lockTtlSeconds the first tick fired after the TTL had already expired — the heartbeat was structurally ineffective for small TTLs, not just in tests. Fixed in the production code (floor lowered to 200ms), not by loosening the tests.

And something in morphium-core you may want to look at, since it's your side: InMemDriver lets any contender steal a still-valid lock, which means acquireLock() is not actually atomic against the in-memory driver. An upsert:true update whose filter matches nothing (because expires_at is still in the future) takes the upsert branch and seeds a document from the equality predicates only — correct, matching MongoDB. But that document then goes through storeInternal(), which treats an already-existing _id as a replace (remove(previous) + insert) rather than raising a duplicate key error. A real server would fail that upsert with E11000; insertInternal() does implement the 11000 path correctly, but the upsert never reaches it. Observed empirically first (upserted=true, n=1 against a lock whose expires_at was still in the future), then confirmed in the driver source.

That has two consequences. It's why the original lockIsRenewedBetweenMigrations could never prove anything, and it's why the obvious "a contender tries to take over and must fail" test is impossible against InMemDriver — I had to rewrite both tests to prove renewal positively instead (read the lock document mid-run, assert owner is unchanged and expires_at has moved past what the original TTL would have given). The test code documents why.

Both tests are mutation-proofed, and that turned out to matter: the first version of the between-units test stayed green with renewLock() disabled — I'd repeated exactly the mistake you called out. Cause: at a 1s TTL the heartbeat ticks every ~333ms, so with 800ms change units the heartbeat was silently renewing the lock and masking the missing between-units call. Raising that test's TTL to 6s makes the heartbeat interval (2s) longer than any single unit, which genuinely isolates the two mechanisms. Now disabling renewLock() reddens only the between-units test, and disabling the heartbeat reddens only the in-flight test — each with its own assertion, the other 8 staying green.

I have not touched morphium-core for this — flagging it rather than fixing it in a Quarkus PR.

Blocker 7 — Docker guard

Two changes. The detector no longer dereferences the Morphium proxy at all: it's not a CDI bean anymore, the StartupEvent observer and its background thread are gone, and the listener is registered from MorphiumProducer.buildMorphium() right after the connect succeeds — so it is now structurally impossible for the detector to cause a connect, rather than merely unlikely. Plus a JUnit ExecutionCondition calling DockerClientFactory.instance().isDockerAvailable() directly, which JUnit evaluates before QuarkusTestExtension boots, replacing the @BeforeAll check.

What I could not prove, and I'd rather say so than claim it: I couldn't produce a genuinely Docker-less run on this machine. Three attempts, each traced to its cause:

  1. DOCKER_HOST — ignored; Testcontainers' UnixSocketClientProviderStrategy checks a hardcoded /var/run/docker.sock.
  2. TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE — works in isolation (isDockerAvailable() → false), but Quarkus's own guard uses ContainerRuntimeUtil.detectContainerRuntime(), i.e. the docker binary, so DockerStatusBuildItem still correctly reported available and Dev Services still tried to start a container.
  3. -Dquarkus-local-container-runtime=UNAVAILABLE — also ineffective: ~/.testcontainers.properties pins the socket strategy and overrides the env var.

Which surfaces something you may care about independently: the project has two different Docker detections that can disagree — Quarkus's binary check versus Testcontainers' socket check. That's very likely also why @EnabledIfDockerAvailable misbehaved under Quarkus test classloading in the first place. If you have a working way to simulate "no Docker" on a machine that has it, I'll happily run the proof; otherwise this needs verifying on a genuinely Docker-less machine or in CI.

Smaller items

  • Multi now rejected alongside Uni. While testing it I found the pre-existing Uni detection had no test at all — only CompletionStage was covered. Both are covered now, via classes defined at runtime with those exact FQNs (ByteBuddy, already on the test classpath) rather than a stub source file in the io.smallrye.mutiny package, which would have collided as a duplicate class the moment Mutiny becomes a real dependency.
  • ssl.tls-configuration-name documented; a sweep confirmed the other eight ssl.* properties were already there.
  • You're right that the blocker 4 commit message and the code disagree about AFTER_ROLLBACK firing for Errors. The code is deliberate (Errors propagate without a lifecycle event); the message is wrong. Noted for the message, no code change.

All six items are pushed. Final verification: full reactor clean build green, 254/254 integration tests green (0 failures, 0 errors, 0 skipped), 59/59 runtime-module and 23/23 deployment-module unit tests green.

Three things I'd flag as genuinely open rather than done:

  • the build-time rejection of Limit/PageRequest on non-FIND prefixes has no automated test yet (needs a synthetic-Jandex unit test in the deployment module),
  • the Docker-less run is unverified on this machine for the reasons above,
  • the InMemDriver upsert/replace behaviour is yours to decide on.

And two questions back at you: whether you want a .tool-versions JDK pin committed, and whether the Limit-on-delete rejection should be a build error as I've implemented it or a silent ignore for compatibility.

@Bardioc1977
Bardioc1977 requested a lite review from Copilot August 6, 2026 15:35
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Hi Stephan,

sorry, our AI agents were told to automatically process incoming reviews and therefore they completed everything, even though you suggested to postpone some minor fixes. However, now almost everything you mentioned has been sorted out with one question regarding InMemoryDriver Support status.

Hope its fine, that we did not stop the agent once you suggested a split in major and minor findings.

Heiko

…ed files

Copilot flagged that the published POMs still carry an
"ENTSCHEIDUNG-OFFEN D1" marker and German-only commentary. That is fair, and
it turned out to be broader than the two spots it named: seven places across
four POMs plus one Java Javadoc referenced internal planning documents
("D1", "D3", "B6", "I4", "Absicherung", "Begleitmassnahmen",
"D3-reactor-strategie.md"). None of those documents exist anywhere in this
repository, so for anyone reading the merged code they are dead pointers.

The ENTSCHEIDUNG-OFFEN markers are removed rather than translated: they
tracked a decision that has since been made and implemented (both modules
inherit their version and morphium.version from morphium-parent, visible in
the <parent> block right below where the marker sat). Everywhere else the
substance of the comment is kept and the document references are replaced by
the reasoning itself, so each comment stands on its own. The reactor's
I1-I5 invariant list is deliberately kept as-is -- it is self-explanatory and
genuinely useful; only its external references were dropped.

morphium-jakarta-data/pom.xml is included because it carries the identical
marker (plus a German "check whether this belongs in morphium-parent" note)
and is part of the same published reactor, even though that module already
merged separately.

Comments only: verified via diff that no version, dependency, property,
module or other XML structure was touched. mvn validate green (that also
catches an accidental double hyphen inside an XML comment, which is invalid
XML -- a trap I had already hit once in this branch), mvn install green,
integration-tests test-compile green.

The other Copilot finding in the same review -- that the extension guide URL
points at the develop branch while the default branch is main -- is a false
positive: this repository's default branch IS develop, so the URL is
correct. It 404s only because the file it points at arrives with this very
PR.

Copilot review comment on PR sboesebeck#267 (sboesebeck/morphium).
@Bardioc1977
Bardioc1977 requested review from sboesebeck and a lite review from Copilot and removed request for Copilot August 6, 2026 15:56

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@Bardioc1977
Bardioc1977 requested a balanced review from Copilot August 6, 2026 18:43

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 148 out of 148 changed files in this pull request and generated no new comments.

Suppressed comments (9)

quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/MorphiumVersionTest.java:1

  • This test claims to cover an entity 'without @Version', but it uses ItemEntity which is annotated with @Version. As written, it doesn't validate the intended scenario and may give false confidence. Suggested fix: either (mandatory) introduce a separate entity class in the IT module without @Version for this test, or (alternative) rename the test/display name + comments to reflect what it actually verifies.
    quarkus-morphium/runtime/src/test/java/de/caluga/morphium/quarkus/health/MorphiumStartupCheckTest.java:1
  • These two tests assert the exact same condition (isEverConnected(0.0, false) == false). Keeping both adds duplication without increasing coverage. Suggested fix: remove one of them, or change one to cover a distinct edge case (e.g. negative/NaN connectionsOpened behavior if that is meaningful for the driver stats).
    quarkus-morphium/runtime/src/test/java/de/caluga/morphium/quarkus/health/MorphiumStartupCheckTest.java:1
  • These two tests assert the exact same condition (isEverConnected(0.0, false) == false). Keeping both adds duplication without increasing coverage. Suggested fix: remove one of them, or change one to cover a distinct edge case (e.g. negative/NaN connectionsOpened behavior if that is meaningful for the driver stats).
    quarkus-morphium/runtime/src/main/resources/META-INF/quarkus-extension.yaml:1
  • The guide URL points at a GitHub develop branch blob, which is likely to drift or become stale for released artifacts. For Quarkus extension metadata, it’s better to link to a stable, versioned documentation location (e.g. a published docs site or a tag/branch that matches releases) so consumers don’t land on unrelated content.
    quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTransactionEvent.java:1
  • The event stores rollback failures as Exception, which excludes non-Exception Throwables (e.g. Error). If the interceptor ever catches/rethrows broader throwables, this API will force lossy wrapping or dropping the cause. Suggested fix: store Throwable (or an Optional-like representation) instead of Exception, and update the accessor/Javadoc accordingly.
    quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTransactionEvent.java:1
  • The event stores rollback failures as Exception, which excludes non-Exception Throwables (e.g. Error). If the interceptor ever catches/rethrows broader throwables, this API will force lossy wrapping or dropping the cause. Suggested fix: store Throwable (or an Optional-like representation) instead of Exception, and update the accessor/Javadoc accordingly.
    quarkus-morphium/runtime/src/main/java/de/caluga/morphium/quarkus/transaction/MorphiumTransactionEvent.java:1
  • The event stores rollback failures as Exception, which excludes non-Exception Throwables (e.g. Error). If the interceptor ever catches/rethrows broader throwables, this API will force lossy wrapping or dropping the cause. Suggested fix: store Throwable (or an Optional-like representation) instead of Exception, and update the accessor/Javadoc accordingly.
    quarkus-morphium/integration-tests/src/test/java/de/caluga/morphium/quarkus/it/StatusStats.java:1
  • This new source file lacks the Apache 2.0 license header that most other newly added Java files in this PR include. If the repository requires license headers for new sources, add the standard header here (and in the other minimal record-only files added alongside it) to keep compliance consistent.
    quarkus-morphium/deployment/src/main/java/de/caluga/morphium/quarkus/deployment/MongoDBStartable.java:95
  • This recompiles the same regex Pattern on every call. Suggested fix: hoist the pattern into a private static final Pattern constant and reuse it here.
    String getReplicaSetName() {
        if (container instanceof MongoDBContainer mongoContainer) {
            String connStr = mongoContainer.getConnectionString();
            Matcher m = Pattern.compile("[?&]replicaSet=([^&]+)")
                    .matcher(connStr);
            return m.find() ? m.group(1) : "docker-rs";
        }
        return null;
    }

@sboesebeck

Copy link
Copy Markdown
Owner

Hi Heiko,

yeah, sometimes AI agents go rogue — so you need to keep an eye on them 😉 Quick status check on the current head (83dafb862) — two of the blockers from the review round above still look unresolved to me:

  1. CREATE_ON_WRITE_NEW_COL still doesn't call setIndexCheck(NO_CHECK), unlike the other three branches — core's default (WARN_ON_STARTUP) stays active and the constructor runs the live ClassGraph scan, so this mode still crashes under native-image.

switch (effectiveIndexCheck) {
case CREATE_ON_STARTUP:
// Disable Morphium-internal creation — Producer.ensureIndices() handles it
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.NO_CHECK);
break;
case WARN_ON_STARTUP:
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.WARN_ON_STARTUP);
break;
case CREATE_ON_WRITE_NEW_COL:
cfg.setAutoIndexAndCappedCreationOnWrite(true);
break;
case NO_CHECK:
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.NO_CHECK);
break;
}

  1. Entity-parameter @Delete methods are still routed through the @By-condition path — the parameter-name fallback in the loop below doesn't check whether the parameter type is the entity type, so @Delete void remove(CustomerEntity customer) builds {customer: <entity>} as a query, matches nothing, and query.delete() silently removes 0 rows while returning normally.

private void generateDeleteAnnotatedMethod(ClassCreator cc, MethodInfo method,
String entityClassName) {
// Check if this is entity-parameter delete or @By-condition delete
boolean hasByParams = false;
StringBuilder conditionsSpec = new StringBuilder();
for (int i = 0; i < method.parametersCount(); i++) {
// Check for @By annotation; fall back to method parameter name if compiled with
// -parameters (Jakarta Data spec §4.6.1) -- same pattern as generateFindAnnotatedMethod.
// Without this fallback, a @Delete method relying on parameter names alone gets
// hasByParams=false, is (mis)treated as an entity-parameter delete, and ends up
// calling doDelete(someString) at runtime -- attempting to delete a String as if
// it were an entity.
AnnotationInstance byAnn = method.parameters().get(i).annotation(BY_ANNOTATION);
String fieldName = null;
if (byAnn != null) {
fieldName = byAnn.value().asString();
} else {
String methodParamName = method.parameters().get(i).name();
if (methodParamName != null) {
fieldName = methodParamName;
}
}
if (fieldName != null) {
hasByParams = true;
if (conditionsSpec.length() > 0) conditionsSpec.append(",");
conditionsSpec.append(fieldName).append(":").append(i);
}
}
String[] paramTypeNames = new String[method.parametersCount()];
for (int i = 0; i < method.parametersCount(); i++) {
paramTypeNames[i] = toDescriptorName(method.parameterType(i));

Everything else from the review round looks addressed. Let me know if I'm looking at a stale view of these two.

flgke81 added 2 commits August 7, 2026 07:59
…eting nothing

Another regression from my own earlier fix, caught by Stephan. The @by
parameter-name fallback I added to generateDeleteAnnotatedMethod checked only
whether a parameter HAS a name, never what its TYPE is. So for

    @delete void remove(CustomerEntity customer)

-- an entity lifecycle parameter, which Jakarta Data says must go to
doDelete(entity) -- the fallback claimed the parameter as a condition, built
the query {customer: <entity>}, matched nothing, and query.delete() removed
zero documents while the method returned normally. Silent data loss, and it
worked correctly before my fallback existed.

The fallback now applies only when the parameter type is not the entity
itself, an array of it, or a List/Collection/Iterable of it. An explicit @by
is still always honoured, even on an entity-typed parameter, since that is a
deliberate opt-in by the developer.

Mixing an entity parameter with condition parameters in one @delete is now
rejected at build time. The Delete javadoc in jakarta.data-api 1.0.1 defines
either exactly one entity/List<E>/E[] lifecycle parameter or condition
parameters, not both, so there is no semantics to implement -- failing the
build beats inventing one.

Tests: 4 new unit tests over a synthetic Jandex index (isEntityParameter
across all type shapes plus negative cases, entity-parameter delete is not
treated as a condition delete, both cases asserted side by side, and the
build-time rejection of the mixed case), plus an integration test asserting
the actual document count before and after remove(OrderEntity) with a
per-customer cross-check -- the test that would have caught this.

Mutation-proofed rather than trusted: disabling the type check makes the
build fail, and instructively so -- remove(OrderEntity) is then misread as a
condition parameter and trips the new mixed-case rejection. The two guards
compose, so even a failing type check cannot resurrect the silent-delete
path.

Still open: the entity branch continues to support only a single entity via
doDelete(Object), not List<E>/E[] via doDeleteAll(List). That predates this
bug and is unrelated to it; noting it rather than bundling it in.

Verified: clean reactor build green, 27/27 deployment-module tests, 255/255
integration tests (0 failures, 0 errors, 0 skipped).

Reported by Stephan Boesebeck on PR sboesebeck#267 (sboesebeck/morphium).
…s two small cleanups

Three findings from the latest Copilot review.

The test named "Entity without @Version stores and updates normally" used
ItemEntity -- which HAS a @Version field -- and asserted getVersion() == 2,
i.e. it verified working version tracking, the exact opposite of its name.
Its own comments admitted it ("re-uses ItemEntity", "pretend no version was
tracked"). It was pure false confidence. There is now a real UnversionedEntity
with no @Version field, and the test proves the actual behaviour: a second
client loads and updates the document in between, and the now-stale original
reference still stores successfully instead of throwing
VersionMismatchException. The build confirms the new entity is picked up (27
instead of 26 @Entity/@Embedded classes registered). The other three tests in
the class keep using ItemEntity, which is correct -- they test versioning.

MorphiumStartupCheckTest had two tests asserting the identical condition
(isEverConnected(0.0, false) == false). Removed the duplicate rather than
repurposing it: the remaining four tests already cover all four boolean
combinations of "connectionsOpened > 0 || driverConnected", and a negative
connection count is not a meaningful case for a monotonic stats counter, so
there was no uncovered edge to move it to. The survivor has the more
descriptive SRV-discovery framing.

MongoDBStartable recompiled the same replicaSet regex on every
getReplicaSetName() call; hoisted into a static final Pattern.

Two further Copilot suggestions were deliberately not taken. Changing
MorphiumTransactionEvent from Exception to Throwable would alter a public API
for a case that intentionally fires no event at all -- the interceptor catches
Throwable but only fires AFTER_ROLLBACK for Exceptions, letting Errors
propagate, which is the agreed behaviour. And the missing license header on
StatusStats matches existing practice in integration-tests, where many files
have none; adding one there alone would be inconsistent rather than more
correct.

Verified: 255/255 integration tests, 58 runtime-module tests (one fewer, the
removed duplicate), clean reactor build green.

Copilot review on PR sboesebeck#267 (sboesebeck/morphium).
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

You were right on the second one, and it was mine. On the first one I still think it's a false positive — but my earlier reasoning for that was wrong, so let me redo it properly.

2. Entity-parameter @Delete — confirmed, fixed

Exactly as you describe, and it came straight out of my own @By fallback fix. The fallback checked only whether a parameter has a name, never what its type is, so @Delete void remove(CustomerEntity customer) got claimed as a condition, built {customer: <entity>}, matched nothing, and query.delete() removed zero rows while returning normally. It worked correctly before I touched it.

The fallback now applies only when the parameter type isn't the entity itself, an array of it, or a List/Collection/Iterable of it. An explicit @By is still always honoured even on an entity-typed parameter — that's a deliberate opt-in.

I also made the mixed case (entity parameter and conditions in one method) a build-time error. The Delete javadoc in jakarta.data-api 1.0.1 defines either exactly one entity/List<E>/E[] lifecycle parameter or condition parameters, not both, so there's no semantics to implement and guessing one seemed worse than failing loudly. Say the word if you'd rather have it accepted with defined behaviour.

Tests: 4 unit tests over a synthetic Jandex index (isEntityParameter across all type shapes plus negatives, entity-parameter delete is not a condition delete, both cases asserted side by side, and the mixed-case rejection), plus an integration test asserting the actual document count before and after the call with a per-customer cross-check — the test that would have caught this in the first place.

I mutation-proofed it instead of trusting the green run, and that was instructive: with the type check disabled the build fails, because remove(OrderEntity) is then misread as a condition parameter and trips the new mixed-case rejection. The two guards compose, so even a broken type check can't resurrect the silent-delete path.

One thing I did not do: the entity branch still only handles a single entity via doDelete(Object), not List<E>/E[] via doDeleteAll(List). That gap predates this bug (it was there before my fallback too), so I left it out rather than bundling an unrelated feature into a regression fix. Happy to add it if you want it in this PR.

1. CREATE_ON_WRITE_NEW_COL — still a false positive, but my old argument was wrong

I owe you a correction here. When I dismissed this the first time I claimed setAutoIndexAndCappedCreationOnWrite(true) overrides the index check to NO_CHECK. That's simply false — MorphiumConfig line 509-511 sets it to CREATE_ON_WRITE_NEW_COL. So my reasoning was wrong even though I landed on the right answer, which is exactly the kind of thing that deserves being called out.

The actual reason no setIndexCheck(NO_CHECK) is needed: the ClassGraph scan lives in checkIndices(), and its only startup call site is guarded to two modes specifically —

Morphium.java:575-576:

if (!...getIndexCheck().equals(IndexCheck.NO_CHECK) && (...equals(IndexCheck.CREATE_ON_STARTUP) ||
        ...equals(IndexCheck.WARN_ON_STARTUP))) {
    Map<Class<?>, List<IndexDescription>> missing = checkIndices(...);   // line 577, the scan

CREATE_ON_WRITE_NEW_COL isn't in that condition, so the scan never runs and the constructor never touches ClassGraph. Core's default WARN_ON_STARTUP doesn't stay active either — setAutoIndexAndCappedCreationOnWrite(true) has already replaced it with CREATE_ON_WRITE_NEW_COL by then. The index/capped creation for this mode happens later on the write path via ensureIndicesFor, which uses plain reflection through AnnotationAndReflectionHelper, not ClassGraph.

So the branch looks asymmetric next to the other three, and I understand why it reads as an oversight, but the asymmetry is load-bearing: adding setIndexCheck(NO_CHECK) there would overwrite the very value that makes the mode work.

If you'd still prefer it explicit, the honest version would be a comment on that branch explaining why it deliberately doesn't set the check — I'm glad to add that, since this is now the second time the branch has looked wrong to a reader.

Also in this push

Three findings from the Copilot review that finally came through (its earlier attempts were erroring out server-side):

  • A test named "Entity without @Version" used ItemEntity, which has @Version, and asserted getVersion() == 2 — it verified the opposite of its name. There's now a real UnversionedEntity, and the test proves the actual behaviour: a concurrent client updates the document in between and the stale reference still stores instead of throwing VersionMismatchException.
  • Two tests in MorphiumStartupCheckTest asserted an identical condition; removed the duplicate (the remaining four already cover all boolean combinations).
  • MongoDBStartable recompiled the same regex per call; hoisted to a constant.

Two Copilot suggestions I deliberately declined: switching MorphiumTransactionEvent to Throwable would change a public API for a case that intentionally fires no event (Errors propagate without a lifecycle event, as we discussed), and the missing license header on StatusStats matches existing practice in that module.

Head is now 7bbe7c790. Clean reactor build green, 255/255 integration tests, 27/27 deployment and 58 runtime unit tests.

@sboesebeck

Copy link
Copy Markdown
Owner

Hi Heiko,

good progress on the latest wave — the entity-parameter @Delete fix (f81e5881) is correct: isEntityParameter() keeps the fallback off entity-typed params, the build-time rejection for mixing entity + @By params is a nice touch, and the boolean/Integer/Long VerifyError guard came along for free. Migration lock and the Copilot cleanup wave (7bbe7c79) look good too.

One item left: CREATE_ON_WRITE_NEW_COL still doesn't call setIndexCheck(NO_CHECK) — same spot as before.

switch (effectiveIndexCheck) {
case CREATE_ON_STARTUP:
// Disable Morphium-internal creation — Producer.ensureIndices() handles it
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.NO_CHECK);
break;
case WARN_ON_STARTUP:
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.WARN_ON_STARTUP);
break;
case CREATE_ON_WRITE_NEW_COL:
cfg.setAutoIndexAndCappedCreationOnWrite(true);
break;
case NO_CHECK:
cfg.collectionCheckSettings().setIndexCheck(CollectionCheckSettings.IndexCheck.NO_CHECK);
break;
}

…COL in native images

Third time Stephan raised this branch, and he was right that it is broken
asymmetry. My two earlier dismissals were both wrong, in different ways:

First I claimed setAutoIndexAndCappedCreationOnWrite(true) forces the index
check to NO_CHECK. It does not -- MorphiumConfig lines 509-511 set BOTH the
index check and the capped check to CREATE_ON_WRITE_NEW_COL.

Then I checked only the index path, found that checkIndices()' ClassGraph
scan is gated to CREATE_ON_STARTUP/WARN_ON_STARTUP (Morphium.java 575-576),
and concluded there was no scan. That gating is real, but I had missed the
second scan: checkCapped() (Morphium.java 3354, scanning at 3358) is called
UNCONDITIONALLY at line 529, with no mode gate at all. Looking at only one of
the two values that setter writes is what hid this from me twice.

Tracing the whole chain, though, changes the severity rather than the
conclusion: buildMorphium() already pre-registers the build-time @CappeD list
into ClassGraphCache before the Morphium constructor runs, and
getClassesWithAnnotation() returns a pre-registered entry without scanning --
including an empty one, and cappedClassNames defaults to
Collections.emptyList(), never null. So the scan was in practice already
unreachable, and native images were not actually crashing here.

The branch is still worth fixing. It now forces both checks to NO_CHECK when
ImageMode.current() is NATIVE_RUN, which makes the safeguard independent of
that call-ordering staying intact and restores symmetry with the other three
branches. A native run cannot create collections on the fly anyway, so
nothing of value is disabled. The JVM path is deliberately untouched: there
the scan is a startup cost rather than fatal, and disabling it would defeat
the whole point of the mode. The comment now spells out this reasoning so the
branch does not read as an oversight a fourth time.

Five unit tests, including a JVM counter-test asserting both checks stay on
CREATE_ON_WRITE_NEW_COL, so the fix cannot silently break the mode for JVM
users. Mutation-proofed: disabling the native guard reddens exactly the
native test on its own assertion and leaves the other four green.

Verified: clean reactor build green, 63/63 runtime tests (58 + 5 new), 27/27
deployment, 255/255 integration (0 failures, 0 errors, 0 skipped).

Reported three times by Stephan Boesebeck on PR sboesebeck#267 (sboesebeck/morphium).
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

You were right, and I was wrong twice — in two different ways. Fixed in 6137e09f8, and here's the full chain, because the interesting part is what I kept missing.

Where I went wrong

First dismissal: I claimed setAutoIndexAndCappedCreationOnWrite(true) overrides the index check to NO_CHECK. Plainly false — MorphiumConfig lines 509-511 set both the index check and the capped check to CREATE_ON_WRITE_NEW_COL.

Second dismissal: I then verified that checkIndices()' ClassGraph scan is gated to CREATE_ON_STARTUP/WARN_ON_STARTUP (Morphium.java:575-576) and concluded there was no scan. That gating is real — but there is a second scan I never looked at:

// Morphium.java:529 — no mode gate at all
var capped = checkCapped();

checkCapped() (line 3354) scans at line 3358 via ClassGraphCache.getClassesWithAnnotation(Capped.class). The switch below it treats CREATE_ON_WRITE_NEW_COL as a no-op, but by then the scan has already run. The root cause of my blindness: that setter writes two values, and I only ever followed the index one.

What tracing it fully changed

Following the chain to the end does shift the severity, and I want to be straight about that rather than overstate the fix: buildMorphium() already pre-registers the build-time @Capped list into ClassGraphCache before the Morphium constructor runs, and getClassesWithAnnotation() returns a pre-registered entry without scanning — including an empty one, and cappedClassNames defaults to Collections.emptyList(), never null. So the scan was in practice already unreachable and native images were not actually crashing here.

That doesn't make your point wrong, it makes it a latent one: the branch leaves both checks on CREATE_ON_WRITE_NEW_COL and relies entirely on that pre-registration ordering holding. Fixing it is cheap and removes the dependency.

The fix

Both checks are now forced to NO_CHECK when ImageMode.current() is NATIVE_RUN — note both, since disabling only setIndexCheck as you suggested would have left checkCapped() reachable. A native run can't create collections on the fly anyway, so nothing of value is disabled, and the branch is symmetric with the other three again.

The JVM path is deliberately untouched. There the scan is a startup cost, not fatal, and switching it off would defeat the entire purpose of the mode for every JVM user. If you'd rather have it off there too for startup time, that's your call.

The comment now spells all of this out — cause, why the pre-registration already covers it, and why the guard is still there — so the branch doesn't read as an oversight a fourth time.

Five unit tests, including a JVM counter-test asserting both checks stay on CREATE_ON_WRITE_NEW_COL, so the fix can't silently break the mode. Mutation-proofed: disabling the native guard reddens exactly the native test on its own assertion, the other four stay green.

Verification

Clean reactor build green, 63/63 runtime tests (58 + 5 new), 27/27 deployment, 255/255 integration — 0 failures, 0 errors, 0 skipped.

Thanks for pushing on this three times instead of letting it go. Two of my "verified false positive" calls in this PR turned out to be incomplete analysis, which is a good argument for you keeping the sceptical eye on the ones I'm still claiming.

@sboesebeck

Copy link
Copy Markdown
Owner

Hi Heiko,

all three blockers are cleanly resolved now, and the CREATE_ON_WRITE_NEW_COL fix went further than what was actually asked for — catching the checkCapped() gap most people (myself included) would have missed, and backing it with a mutation-proofed test so it can't regress silently. That's exactly the kind of thoroughness this extension deserves.

Genuinely impressed by this whole PR: a clean module split, careful attention to native-image and Dev Services edge cases, and — maybe the best part — you took a deep, occasionally uncomfortably thorough review completely in stride, fixed everything properly instead of the fast/shallow version, and even called out where the review itself had gaps. That's a rare combination. Thank you for the effort and the patience.

Merging now. Really looking forward to quarkus-morphium shipping in 6.3.0 — and to spring-boot-morphium after it.

@sboesebeck
sboesebeck merged commit a8d7d8e into sboesebeck:develop Aug 7, 2026
1 check passed
@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

Hi Stephan,

please hold back releasing 6.3.0 - We've come up with a regression in two of our applications when switching to the 6.3.0-SNAPSHOT. As far as I see its a very old bug. A fix is on the way.

@sboesebeck

Copy link
Copy Markdown
Owner

yes - I was running the whole test suite anyways and found a flaky test I want to investigate. I will post an announcement before creating the release! 😉

@Bardioc1977

Copy link
Copy Markdown
Collaborator Author

I let Copilot do its first round on our fork:

Bardioc1977#19

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