Replace isMapValueTypeObject + isMapStringString with isBuiltinJsonMap:
the built-in JSON map handles a String or enum key with a String, Object
or wildcard value. This also routes Map<Enum,String> to the built-in type
and fixes non-String/enum keys with Object value (e.g. Map<Integer,Object>)
being mis-routed to the built-in type (ClassCastException) instead of the
object mapper.
Addresses #3735. Enum-keyed JSON maps previously failed (enum key cast to
String). Add ScalarTypeJsonMapEnum which converts enum keys via their
ScalarType (honouring @DbEnumValue) and reuses ScalarTypeJsonMap for all
storage/platform handling, so it works for VARCHAR/CLOB/BLOB and Postgres
JSON/JSONB with no per-storage variants.
- JsonStorage now reports jdbcType(), enabling a shared storageFor(...) reused
by the plain and enum-key Map types
- DefaultTypeManager routes Map<Enum,Object> to the new type and calls
setAccessible on the @DbEnumValue method (supports nested/non-public enums)
- add TypeReflectHelper.getMapKeyTypeRaw and TestEnumKeyMap
Collapse the per-storage and per-platform ScalarType subclass explosion for the built-in JSON value types into two orthogonal strategies.
- Add JsonStorage strategy (VARCHAR / CLOB / BLOB / Postgres) encapsulating
how the raw JSON string is read from / bound to JDBC. Postgres vs
non-Postgres is now a single reusable strategy rather than a subclass per
value type.
- Add ScalarTypeJsonValue<T> base holding the shared read / bind / L2-cache /
json plumbing once, plus ScalarTypeJsonCollectionValue<T> adding the
ScalarTypeArray (DB array column definition) aspect for List/Set.
- Rewrite ScalarTypeJsonMap, ScalarTypeJsonList and ScalarTypeJsonSet as thin
types: a typeFor factory selecting a JsonStorage + value marshalling that
delegates to the avaje-JsonMapper-backed EJson facade.
- Remove ScalarTypeJsonMapPostgres and ~16 nested storage/platform classes.
Reworks the DJsonService (the SpiJsonService SPI behind io.ebean.text.json.EJson) to use avaje JsonMapper instead of the bespoke EJsonReader/EJsonWriter, consolidating all read/write logic into a single JsonAdapter.
Changes
- New EbeanJsonAdapter — a JsonAdapter<Object> that materializes JSON into plain or modify-aware Map/List/Set, with two shared singletons (PLAIN, MODIFY_AWARE). Preserves existing EJson semantics: - Integral numbers → Long, decimals → BigDecimal
- Write coverage for String/Integer/Long/Double/Float/BigDecimal/Boolean/Map/Collection with a toString() fallback for other types (UUID, enum, etc.)
- Modify-aware loads share a single ModifyAwareFlag owner per root, reset to non-dirty once after the load completes
- DJsonService now builds one JsonMapper + two JsonMapper.Type<Object> and routes parse/write through them. Null serialization is retained via a serializeNulls(true) writer; null/blank-input guards, token-honoring entry points, and parseSet (modify-aware asSet()) are preserved.
- EJsonReader/EJsonWriter are now unused and should be deleted (couldn't remove them in this environment).
Why
Simplifies Ebean's JSON handling by reusing avaje-json-core's JsonMapper rather than maintaining a parallel reader/writer, while keeping behavior identical.
This allows for customisation of the built in I__ init scripts.
For the case of postgres db partitions a customisation to support
unlogged tables was for example always being overwritten.
This change makes the I__ scripts a "add if not already exists".
Note that R__ repeatable scripts are "always overwrite"
* Redesign of metric labels - secondary queries (lazy|query) now just use parent + relativePath + type
# Query metric/plan label change — comparison
Improves the metric/plan name generated for **secondary** (`_lazy` / `_query`) loads so
they relate to their parent/root query, and unifies separators on `.`.
The secondary lazy name is now **always** `orm.<parent's full name>.<path>.<loadMode>`,
so it always prefixes the real parent metric.
## Root query name
| Root query source | Original | New |
|---|---|---|
| `setLabel("custMain")` | `orm.Customer_custMain` | `orm.Customer.custMain` |
| ProfileLocation `CustomerFinder.byName` | `orm.CustomerFinder.byName` | `orm.CustomerFinder.byName` *(same)* |
| ProfileLocation `DataLoader.loadAll` (Customer query) | `orm.Customer_DataLoader.loadAll` | `orm.Customer.DataLoader.loadAll` |
| Unlabeled, no location | `orm.Customer.findList` | `orm.Customer.findList` *(same)* |
## Secondary lazy (`contacts`) name
Original prefixes the **loaded** type (`Contact`) + the call-site location and uses `__`
between path and load mode. New prefixes the **parent's full name**.
| Root query source | Original lazy name | New lazy name |
|---|---|---|
| `setLabel("custMain")` *(profile location also present)* | `orm.Contact_CustomerFinder.findActive_contacts__lazy` — explicit label **lost** | `orm.Customer.custMain.contacts.lazy` |
| `setLabel("custMain")` *(no profile location)* | `orm.Contact_custMain_contacts__lazy` | `orm.Customer.custMain.contacts.lazy` |
| ProfileLocation `CustomerFinder.byName` | `orm.Contact_CustomerFinder.byName_contacts__lazy` | `orm.CustomerFinder.byName.contacts.lazy` |
| ProfileLocation `DataLoader.loadAll` | `orm.Contact_DataLoader.loadAll_contacts__lazy` | `orm.Customer.DataLoader.loadAll.contacts.lazy` |
| Unlabeled, no location | `orm.Contact.findList` *(own name; no parent path)* | `orm.Contact.findList` *(same)* |
## Problems fixed
- **(A) Secondary load didn't relate to its parent** — original prefixed the *loaded* type
(`Contact`) + the *call-site* location, never the parent query's name. New name literally
starts with the parent's full name.
- Explicit `setLabel` was **silently dropped** for secondary queries when a profile location
existed.
- `__` path/loadMode separator and mixed `_`/`.` replaced by uniform `.`.
## Nested and `.query` secondary loads
Each secondary query name is `<immediate parent's full name>.<immediate path>.<loadMode>`,
so every hop literally prefixes its parent metric. Example with root `setLabel("custMain")`
on `Customer`, chain `Customer -> orders -> details`:
Nested lazy:
```
orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy
```
Secondary eager `.query` fetch:
```
orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query
```
The intermediate load mode (`.lazy.` / `.query.`) is retained so each name is an exact
extension of its immediate parent's name.
* Use ProfileLocation as leading metric name without <type> prefix
* docs: Add guide for ebean-query-metrics.md
* docs: Add guide for query plan capture