Implements the mongodb-superpowers-package design: Stable Tasks 1-50 and Advanced Tasks 1-15. The design assumes 19 Stable + 12 Advanced Gradle projects under modules/mongodb*. This repository's fail-closed registry declares exactly 19 leaf identities, so those modules become package boundaries inside the registered leaf :adapter:outbound:persistence-mongo, with the design's module dependency table enforced by ten ArchUnit rules. The mapping and every deviation are recorded in docs/mongodb/repository-adaptation.md. Contract highlights, all enforced by tests rather than convention: - Transaction body retry and commit retry are separate loops. A new session per body attempt; commit-only retry on an unknown commit. The body is never replayed after a commit ambiguity, so a failover cannot become a duplicate. - MongoExecutionOutcome keeps both ambiguous outcomes distinct from success and failure, and MongoFailureContext records only the design-permitted fields. - Failure classification reads server error labels before numeric codes. - BSON representations come from a pinned manifest, never a library default, and a golden type-signature gate fails on any drift. - Index and validator changes go through the manifest and the admin plane; metadata ownership gates every drop. - Every Advanced capability refuses construction unless its flag is enabled. Verified against real servers, not only unit tests. Running the lanes for the first time exposed four defects that a green `check` had hidden: - Four release lanes passed while executing zero tests; the gate now counts executed tests per lane and fails on zero. - The "single replica set" fixture was a standalone, because Testcontainers 2.x needs withReplicaSet(); its test only asserted a connection string. - The three-node fixture was three independent clusters, so no election could occur, and awaitNewPrimary() compared against the post-stop primary. - The migration lease checked modifiedCount, so a same-millisecond refresh read as a lost lease. scripts/verify-mongodb-platform.sh now reports: 9 lanes, 0 skipped, 0 failed, every evidence category produced. scripts/verify-mongodb-advanced.sh reports NOT PROMOTABLE: actual-topology evidence (real sharded cluster, real KMS, real target deployment) is unobtainable here, so it is named rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
93 lines
4.5 KiB
Markdown
93 lines
4.5 KiB
Markdown
# BSON Mapping Guide
|
|
|
|
Design §10 and decision D-06. The representation of a value in BSON is a data contract, not an
|
|
implementation detail: once a collection holds a million documents, changing how a `BigDecimal` is
|
|
stored is a migration with downtime, not a code change. `MongoTypeRepresentationManifest` pins the
|
|
representation so a library upgrade or a different default cannot move it.
|
|
|
|
## 1. The manifest
|
|
|
|
`MongoTypeRepresentationManifest.standard()` fixes:
|
|
|
|
| Java type | BSON | Representation type |
|
|
|---|---|---|
|
|
| `UUID` | `Binary` subtype 4 | `MongoUuidRepresentation.STANDARD` |
|
|
| `BigDecimal` | `Decimal128` | `MongoDecimalRepresentation.DECIMAL_128` |
|
|
| `BigInteger` | `Decimal128` (or `String` when out of range, declared) | `MongoBigIntegerRepresentation` |
|
|
| `Instant` / `OffsetDateTime` / `ZonedDateTime` | UTC `Date` | `MongoTemporalRepresentation.UTC_DATE` |
|
|
| `LocalDate` | `String` (ISO-8601) or UTC `Date`, declared per field | `MongoTemporalRepresentation` |
|
|
| `enum` | `String` name | `MongoEnumRepresentation.NAME` |
|
|
|
|
`MongoMappingConfiguration` and `MongoCustomConversionsFactory` build the Spring Data converters from
|
|
the manifest, so there is one place to read and one place to change.
|
|
|
|
## 2. UUID
|
|
|
|
`UuidRepresentation.STANDARD` (subtype 4), always. The driver's legacy Java representation
|
|
(subtype 3) byte-swaps two halves of the UUID, so a document written by one representation and read
|
|
by the other yields a different — and valid-looking — UUID. Nothing errors; you just get the wrong
|
|
row. The golden snapshot kit pins the codec explicitly for this reason
|
|
(`MongoBsonSnapshot.defaultRegistry()`).
|
|
|
|
## 3. Decimal
|
|
|
|
`BigDecimal` → `Decimal128`, never `Double`. `12.30` stored as a double is `12.299999999999999`, and
|
|
a monetary comparison written against it will one day be wrong by a cent for a customer who notices.
|
|
`BigDecimalToDecimal128Converter` / `Decimal128ToBigDecimalConverter` are registered from the
|
|
manifest.
|
|
|
|
`Decimal128` has 34 significant digits; a `BigDecimal` beyond that range fails on write rather than
|
|
rounding silently.
|
|
|
|
## 4. Time
|
|
|
|
Store instants, not local times. `LocalDateTimeMappingGuard` refuses `LocalDateTime` fields on a
|
|
mapped document: a `LocalDateTime` has no offset, so the value that goes in depends on the JVM
|
|
default zone of whichever instance wrote it, and the two instances in a rolling deploy can disagree.
|
|
Use `Instant` when the moment matters and `LocalDate` when the calendar day matters.
|
|
|
|
## 5. Type metadata
|
|
|
|
`MongoTypeMetadataPolicy` decides what goes in `_class`:
|
|
|
|
| Policy | Stored | Use when |
|
|
|---|---|---|
|
|
| `NONE` | nothing | The collection holds exactly one type and never will hold a subtype. |
|
|
| `ALIAS` | a registered short alias | A polymorphic hierarchy in a long-lived collection. |
|
|
| `CLASS_NAME` | the FQCN | Short-lived or internal collections only. |
|
|
|
|
`PolicyAwareMongoTypeMapper` enforces it, and `MongoTypeMetadataRegistry` holds alias → class.
|
|
A `@LongLivedMongoDocument` type with `CLASS_NAME` is refused: writing `com.example.OrderV2` into a
|
|
million documents means that renaming the package is a data migration.
|
|
|
|
## 6. Missing versus null
|
|
|
|
The golden kit keeps these apart deliberately. `MongoBsonSnapshotAssert.hasNoField(...)` and
|
|
`hasExplicitNull(...)` are different assertions, because in MongoDB they are different documents:
|
|
`{"a": null}` matches `{a: null}` and `{a: {$exists: true}}`, while `{}` matches only the first.
|
|
A mapper change that starts writing explicit nulls silently changes what your queries return.
|
|
|
|
## 7. Golden representation tests
|
|
|
|
Every collection with a fixed representation should have a snapshot test:
|
|
|
|
```java
|
|
MongoBsonSnapshot snapshot = MongoBsonSnapshot.of(storedDocument);
|
|
MongoBsonSnapshotAssert.assertThat(snapshot)
|
|
.hasBsonType("amount", "DECIMAL128")
|
|
.hasBsonType("externalId", "BINARY")
|
|
.hasNoJavaClassName("dev.caskeleton")
|
|
.hasTypeSignature("_id:OBJECT_ID,amount:DECIMAL128,createdAt:DATE_TIME,externalId:BINARY");
|
|
```
|
|
|
|
`hasTypeSignature` is the regression gate: it fails on *any* representation change, including ones a
|
|
value-equality assertion would pass. When it fails, the question is whether the change was intended
|
|
and has a migration — not whether to update the string.
|
|
|
|
## 8. Round trips
|
|
|
|
`MongoRoundTripContract` asserts that `write → read` returns an equal domain object *and* that
|
|
`write → read → write` produces an identical BSON document. The second half is what catches an
|
|
asymmetric converter: a value that reads back equal but re-serialises differently makes every
|
|
subsequent `save()` a spurious update, and turns change streams into a noise generator.
|