Commit Graph
1599 Commits
Author SHA1 Message Date
robin.bygrave 72ce587629 Add dbName() to MetaQueryPlan - easier to support multi-db query plan capture handling 2026-08-14 18:03:02 +12:00
robin.bygrave 77e98a3f54 Metrics - support both CUMULATIVE and DELTA metrics collection concurrently
ebean insight [and StatsD] work off DELTA metrics, where as OTEL
and Prometheus want CUMULATIVE. With this change we can have a metrics
collection for ebean insight using DELTA mode and have a second collection
use CUMULATIVE for reporting to OTEL - for the case of fan-out metrics
going to 2 places.

This isn't strictly needed when only one collection mode is used.
2026-08-14 16:31:28 +12:00
Rob Bygrave 46a73f6250 Version 18.4.0 2026-07-28 22:29:48 +12:00
Rob Bygraveandrobin.bygrave 49f6bef2fb Add ebean-avajejsonb-mapper module - Mapping DbJson content using avaje jsonb library (no reflection, graalvm support) (#3871)
* Add ebean-avajejsonb-mapper module - Mapping DbJson content using avaje jsonb library (no reflection, graalvm support)

* Fix flakey test TestInheritance

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-24 10:01:03 +12:00
Rob Bygraveandrobin.bygrave d6ba4967b6 ORM Update - add usingTransaction() to support using explicit transaction (#3868)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-22 09:45:54 +12:00
Rob Bygraveandrobin.bygrave 0d0ec79f1e SqlUpdate - add usingTransaction() to support using explicit transaction (#3867)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-21 09:59:23 +12:00
Rob Bygraveandrobin.bygrave 1737ae13db Modify SqlQuery.TypedQuery to implement FindableQuery (common interface) (#3866)
Add usingConnection() and canel() to support common FindableQuery interface

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-20 23:27:51 +12:00
Rob Bygraveandrobin.bygrave 17b641b163 MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery) (#3865)
* MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery)

* MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery)

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-20 23:12:37 +12:00
robin.bygrave 37567e8fe8 Change MappedQuery to extend CancelableQuery - make it cancelable 2026-07-20 21:50:59 +12:00
robin.bygrave 14e6e3f824 With InsertOnConflict fallback to PK constraint when no Unique constraint and id value provided
This is for the case where the primary key is a supplied value (like a string, iso code, serial
number, bar code etc)
2026-07-16 23:51:29 +12:00
robin.bygrave 857e92583d enh: Add findOneOrThrow() with decent derived message for EntityNotFoundException
Adding this as I am seeing a LOT of cases where ebean can derive a good enough
error message (id case, plus single unique key case) that it is worth adding
this syntax sugar.

Yes we have discussed this a well it's not so useful for the multi-lingual /
 non-english case and also marginal when the error message needs to be built
 case.
2026-07-16 23:15:15 +12:00
robin.bygrave 149ed8318d Fold id-only *ToOne fetches into parent select, avoiding unneeded join (#3643)
When a *ToOne association is fetched with only its id property, the
foreign key column already exists on the owning table so no join is
required. Previously this always generated an unnecessary join.

- OrmQueryProperties: add includesExactly() and withAddedInclude() to
  support adding an include without mutating a shared/cached instance
  (the included set and its precomputed hash prefix are immutable
  since #3761/#3763, so an "add" must produce a new instance).

- OrmQueryDetail: add convertIdFetches(), a bottom-up pass over
  fetchPaths that replaces an id-only *ToOne fetch with its FK
  property folded into the parent's select, removing the now-unneeded
  fetch path. Skips exported (mappedBy) one-to-one associations, which
  have no local FK column and always require a join. Tracks paths
  still depended on by a surviving child fetch so their join isn't
  incorrectly folded away.

- Add TestFetchIdOnly covering PathProperties, select(), and fetch()
  usages, plus a control case confirming the join is retained when
  more than the id is fetched.

- Update EqlParserTest, TestSubQuery and TestMergeCustomer assertions
  to reflect the new join-free SQL.
2026-07-16 22:28:42 +12:00
Rob Bygraveandrobin.bygrave b6225b6ae4 InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated) (#3855)
* Version 18.3.0

* InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated)

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 17:14:40 +12:00
robin.bygrave f38f0ffa25 InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated) 2026-07-16 17:12:46 +12:00
Rob Bygraveandrobin.bygrave 6c939d72f8 dto extensions - usingMaster, findStream etc (#3854)
* DtoQuery - add usingMaster, usingTransaction, usingConnection options.

* DtoQuery - Support DtoPath renaming a ToOne or ToMany

* MappedQuery - Add findStream() support

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 17:04:22 +12:00
robin.bygrave d083b27e91 Version 18.3.0 2026-07-16 16:57:52 +12:00
Rob Bygraveandrobin.bygrave 30314f163d DtoQuery - add usingMaster, usingTransaction, usingConnection options. (#3853)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 15:50:07 +12:00
Rob Bygraveandrobin.bygrave 636bb28df3 #2540 Add DTO mapping from Entity beans via generated mapper code (#3852)
* #2540 Add DTO mapping from Entity beans via generated mapper code

A large feature, refer to the docs / guides etc. This is a bit like MapStruct in that it generates mapping code via annotation processing but it ALSO creates an optimised FetchGroup for the destination DTO graph and it also implements a generic interface such that it can be fluently used via query.mapTo().

The value is that it automates the mapping code from entity -> dto plus optimises the query used to support building the DTO graph.

Expected to be better to use than MapStruct for ebean orm users.

* Bump ebean-annotation to 8.6 with the dto annotations

* Adjust the build order to via test dependency on ebean-api

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 08:40:41 +12:00
Rob Bygraveandrobin.bygrave 84a311e521 Bug: filterMany() expressions when additional nested fetch has predicates in wrong place (#3851)
* Bug: filterMany() expressions when additional nested fetch has predicates in wrong place

When a filterMany() expression only references the many-root's own properties (e.g. .eq("status", ...)),
but the query also has an additional nested fetch beneath that many-path (e.g. .fetch("orders.customer")
or .fetch("contacts.group")), the filter predicate was incorrectly attached to the last/deepest join's
ON clause instead of the many-root's own join — silently misapplying the filter.

Fix: A hybrid approach:

- If the filterMany expression references only root-level properties → attach the predicate directly on the many-root's own join (correct).

 - If it references deeper/nested properties requiring "extra joins" (a separate existing mechanism) → fall back to the original end-of-subtree behavior, since no SqlTreeNode exists to attach to.

 - includeFilterMany() made idempotent so both mechanisms coexist safely.

* Bug: filterMany() with nested-property predicates silently ineffective
filterMany() expressions referencing a nested/dotted property (e.g.
.eq("group.name", "x"), alone or mixed with root properties) were
attached to a LEFT JOIN's ON clause via the "extra join" mechanism.

That only nulls the extra join's own columns on non-match - it does
not exclude the parent/many-side row, so the predicate had no actual
filtering effect (confirmed by data, not just SQL shape).

## Fix:

force any many-property fetch whose filterMany expression
references a nested property onto a query-join (secondary select
restoring filterMany's original always-fetchQuery behaviour for this
with a genuine WHERE clause) instead of leaving it as an inline JOIN, case.

- SpiExpressionValidation: track all visited property names (allProperties()), not just unknown ones.
- OrmQueryProperties: filterManyHasNestedProperty() walks the filterMany expression for any dotted property reference.
- OrmQueryDetail.markQueryJoins(): route nested-property filterMany chunks to markForQueryJoin() instead of the inline fetch-join slot, without consuming that slot for other many-paths.

Also simplifies the earlier root-only-predicate join-placement fix
(CQueryPredicates/DefaultDbSqlContext/SqlTreeNodeManyRoot) now that
a nested-property filterMany can never reach the JOIN-based code
path: removed the now-unreachable "deepest path" fallback branch,
the includeFilterMany() idempotency guard, and the redundant fallback
call in SqlTreeNodeManyRoot - isFilterManyAttachPoint() remains as
the single, always-used attach mechanism.

Updated TestQueryFilterMany/TestQueryFilterManySimple assertions to
expect the corrected query-join (2 statement) behaviour.

* Simplify SpiExpressionValidation with boolean nestedProperty flag

* Tidy up comments

* Fix OrmQueryProperties.filterManyHasNestedProperty() with real nested property check

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-15 23:16:18 +12:00
Rob Bygraveandrobin.bygrave 791ac13750 Fix findCount()/exists() wrapping a raw CTE (WITH clause) for SQL Server (#3850)
SQL Server does not support a WITH clause (CTE) nested inside a subquery
or derived table - only at the start of a statement. buildRowCountQuery()
and buildExistsQuery() wrap raw sql as `select count(*) from (<sql>)` /
`select case when exists(<sql>) then ...`, which breaks when <sql> starts
with a CTE header (e.g. RawSqlBuilder.withPlaceholders() queries), causing
"Incorrect syntax near the keyword 'with'" on SQL Server.

Add CQueryBuilder.topLevelSelectStart()/splitCteHeader() to detect and
hoist a leading WITH clause in front of the wrapping SELECT rather than
wrapping it along with the rest of the query. This is a no-op for the
common case (no leading CTE) and is portable across platforms.

- ebean-core: CQueryBuilder - hoist leading CTE header in
  wrapSelectCount()/wrapSelectExists()

- Add unit tests in CQueryBuilderTest reproducing the exact failing SQL

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 15:10:57 +12:00
Rob Bygraveandrobin.bygrave d54a24b27d #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0 (#3849)
* #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0

Fix #3848: exists() generates invalid scalar SQL on SQL Server and Oracle

Query.exists() generated `select exists(<subquery>)`, which is valid on
Postgres/H2/MySQL but rejected by SQL Server and Oracle since both only
support EXISTS as a predicate, not as a directly selectable scalar boolean
expression.                                                                                                                                                                              ┃
                                                                                                                                                                                            ┃
Add DatabasePlatform.existsWithCaseWhen/existsFromClause capability flags
and use them in CQueryBuilder.wrapSelectExists() to generate
`select case when exists(<subquery>) then 1 else 0 end` on platforms that                                                                                                                ┃
need it, with an additional ` from dual` suffix for Oracle (which requires                                                                                                               ┃
a FROM clause on every select). CQueryExists reads the boolean result via                                                                                                                ┃
ResultSet.getBoolean(1), which correctly interprets the resulting 0/1 int.

* Fix tests TestInsertCheckUnique for Oracle and SQL Server

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 10:13:16 +12:00
Rob Bygraveandrobin.bygrave bdbe743f56 #3275: Fix NPE when update/delete with @IdClass mapping (#3846)
## Root cause:

For @IdClass composite keys, the "id" is cached only in EntityBeanIntercept.ownerId(), populated only when reading from DB or during insert derivation. A fresh bean instance passed straight to DB.update()/DB.delete() had this null, causing an NPE in BindableIdEmbedded.dmlBind().

## Fix:

dmlBind() now defensively derives the id (reusing the existing insert-time derivation logic, extracted into a shared deriveId() helper) when it's null but derivable — covering both update and delete, since they share the same DML bind path.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 23:10:54 +12:00
Rob Bygraveandrobin.bygrave ed2026b858 #3110: Fix cache key with enum type (#3845)
## Root cause:

Finder.byId() on a @Cache entity converts the id to a String cache key via IdBinder.cacheKey() = scalarType.format(value). For enums, format() returns .name() (e.g. "APPROVED"), not the db-mapped value. On a  cache hit, BeanDescriptorCacheHelp.loadBeanDirect() converts that cache-key string back via desc.convertId() → IdBinderSimple.convertId() → scalarType.toBeanType(), which expects a db value (e.g. "2" for @EnumValue("2")),  not the enum's Java name — causing Integer.parseInt("APPROVED") to crash in EnumToDbIntegerMap.getBeanValue().

## Fix:

IdBinderSimple.convertId() now special-cases String input to use scalarType.parse() (the true inverse of  format()) instead of toBeanType() (the inverse of toJdbcType()/db-value). This mirrors an existing, already-correct pattern in IdBinderEmbedded.convertId(), which already handles String cache-key round-tripping via parse(). Verified  this doesn't affect other id types (Long/Integer/etc.) since parse() and toBeanType() are equivalent for them — the mismatch only exists for Enum-mapped types.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 22:42:16 +12:00
Rob Bygraveandrobin.bygrave 8d21f91f62 #2477: Support @DbArray inside an @Embeddable (#3844)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 21:45:11 +12:00
Rob Bygraveandrobin.bygrave 6d1d58b8c2 #1989: Add findPagedList() support to DtoQuery (when based on orm query) (#3843)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 20:34:25 +12:00
Rob Bygraveandrobin.bygrave 94f252f423 #1700: Add support for @OrderColumn on @ManyToMany (#3841)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 15:55:03 +12:00
Rob Bygrave 275f8ad9f9 #2393: @OrderColumn was not maintained for @ElementCollection — no order column populated on insert and no order by on fetch. (#3840)
## Root cause:
AnnotationAssocManys only parsed @OrderColumn inside the @OneToMany branch, and even if parsed, BeanDescriptorManager.checkMappedByOneToMany() returned early for element collections before reaching the order-column setup. The element collection's synthetic descriptor also isn't registered in deployInfoMap, so it couldn't reuse the existing @OneToMany lookup path.

## Fix (in ebean-core):

- BeanDescriptorManager: extracted makeOrderColumn(DeployBeanPropertyAssocMany, DeployBeanDescriptor) so the synthetic order property can be built against an explicit target descriptor, not just one looked up via deployInfoMap.

- AnnotationAssocManys.readElementCollection(): now reads @OrderColumn, sets fetchOrderBy, and builds the order property directly on the element descriptor before it's converted to a runtime BeanDescriptor.

- SaveManyElementCollection: binds a sequential 0-based index as the order column value on insert (element collections delete-then-reinsert the whole collection in list order, so this is a natural fit).

DDL generation and the fetch order by needed no changes - both already generalize over any BeanDescriptor's order column property once it exists.

Added EcolPerson / TestElementCollectionOrderColumn covering insert population, order preserved on reload, and order by present on fetch-join SQL.

Known limitation: @OrderColumn on a Map-based element collection (SaveManyElementCollectionMap) is not wired up - not part of this issue and not a standard JPA combination (Maps use @MapKeyColumn).
2026-07-13 15:11:49 +12:00
Rob Bygrave 9ac3bd71f6 Issue #2840: @DbJson/@DbJsonB Map<String,Object> fields ignored mutationDetection and always used ModifyAware-wrapper dirty checking — so NONE (and HASH) had no effect, unlike plain Object-typed @DbJson fields which correctly honored these modes. (#3839)
Root cause: The built-in ScalarTypeJsonMap/List/Set types (used for Map<String,Object>, simple List/Set) hard-coded mutable()=true and isDirty() to always do a ModifyAware check, regardless of the configured MutationDetection. Only SOURCE accidentally worked because it happened to route through BeanPropertyJsonMapper.

 Fix (in ebean-core, io.ebeaninternal.server.type):

 - ScalarTypeJsonValue now derives mutable (false only for NONE) and keepSource/jsonMapper() (true for HASH/SOURCE) from the property's MutationDetection, instead of a single hard-coded keepSource boolean.

- ScalarTypeJsonMap, ScalarTypeJsonMapEnum, ScalarTypeJsonList, ScalarTypeJsonSet, and ScalarTypeJsonCollectionValue now accept/pass through MutationDetection instead of a raw boolean.

- DefaultTypeManager.dbJsonType() passes the property's own mutationDetection() straight through for these collection types (per your choice: DEFAULT still always means ModifyAware for collections — only an explicit NONE/HASH/SOURCE annotation changes behavior; the DatabaseConfig-wide default is not applied to these types).

- @DbArray fallback call sites (PlatformArrayTypeJsonList/Set) updated to pass MutationDetection.DEFAULT, preserving unchanged behavior.
2026-07-13 10:24:25 +12:00
Rob Bygraveandrobin.bygrave bb46c88db1 Fix findVersions() unbound bind parameters on sql2011 history platforms (#3837)
On standards-based platforms (MariaDB, SQL Server, Oracle, DB2, HANA) the root table always generates a 'for system_time between ? and ?' clause for TemporalMode.VERSIONS, but bind values were only supplied  for findVersionsBetween(), leaving findVersions() with 2 unbound placeholders and shifted bind positions ("Parameter at position N is not set").

 Match the bind guard to the SQL generation condition and default start/end to epoch/now when not explicit.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-07 21:05:22 +12:00
robin.bygrave 007409d21c Bump postgresql to 42.7.11 2026-07-07 09:21:14 +12:00
Rob Bygrave 8b61069b3f #3441 Handle type coercion in hits against bean cache (#3835) 2026-07-06 22:04:09 +12:00
Rob BygraveandRoland Praml a82dcb2509 Fix #3041 check uniqueness cache skipclean (#3834)
* checkUniqueness supports queryCache and skipClean

* Keep the DB.checkUniqueness() as per original (not overload there)

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 22:03:52 +12:00
Rob BygraveandRoland Praml 08bb170cb2 Fix #3156 inherited property access (#3833)
* Properties of inherited models can be used on different child models

* Adjust typos only

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 21:08:35 +12:00
Rob Bygraveandrobin.bygrave a2f954a60e #3529 - Fix for M2M property is empty in preDelete of BeanPersistAdapter (#3830)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-06 19:47:37 +12:00
Rob Bygrave e525d4e159 Version 18.2.0 2026-07-03 19:38:05 +12:00
Rob Bygraveandrobin.bygrave 4e639cb88a Add deletePermanent() - hard delete for soft delete capable beans (#3828)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 18:23:18 +12:00
Rob Bygraveandrobin.bygrave fc78b2af80 Add RawSqlBuilder.withPlaceholders() for CTEs and other complex SQL (#3649) (#3827)
* Add RawSqlBuilder.withPlaceholders() for CTEs and other complex SQL (#3649)

RawSqlBuilder.parse() uses keyword-based scanning to locate SELECT
columns and the WHERE/HAVING injection points. This fails for CTEs,
window functions and other complex SQL where "select"/"from" keywords
appear in places the scanner doesn't expect.

withPlaceholders(sql) skips SELECT/FROM column parsing entirely and
only locates the ${where}, ${andWhere}, ${having} and ${andHaving}
placeholder positions, requiring explicit columnMapping() calls (as
with unparsed()). This lets dynamic where()/having() expressions be
injected into otherwise unparseable SQL.

Also fixes two bugs in the underlying placeholder-position splitting:
- static SQL following a ${having}/${andHaving} placeholder (e.g. a
  trailing ORDER BY) was silently dropped when both a where and a
  having placeholder were present
- using only ${having}/${andHaving} (no where placeholder) caused a
  dynamically added HAVING clause to be appended after trailing
  static SQL, producing invalid SQL

Changes:
- RawSqlBuilder.withPlaceholders(sql) + SpiRawSqlService.withPlaceholders()
- DRawSqlParser.parseAsTemplate() / parseTemplate() - placeholder-position
  only parsing, correctly splitting preWhere/preHaving/trailing SQL
- CQueryBuilderRawSql - skip column-list and "select" prefix handling
  in template mode (signalled by an empty preFrom)
- Unit tests in DRawSqlServiceTest covering where/andWhere/having/andHaving
  placeholder combinations, including the two fixed edge cases
- Integration tests in TestRawSqlWithPlaceholders (ebean-test) covering
  CTE queries with dynamic where/having and verifying generated SQL

* Add examples test using query beans

* Add docs guides for RawSql

* Add ${orderBy} ${andOrderBy}

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 18:22:42 +12:00
Rob Bygraveandrobin.bygrave 29647ebe1a Fire BeanPersistController when only ManyToMany collection changes (#3652) (#3826)
When only a @ManyToMany collection is modified and saved, the parent bean has no dirty scalar properties so BeanPersistController preUpdate/postUpdate were never invoked — only the junction table rows were written.

Fix mirrors the existing preElementCollectionUpdate() mechanism: add
preManyToManyUpdate() on PersistRequestBean and call it from
SaveManyBeans.saveAssocManyIntersection() when the intersection actually
changes (vanillaCollection, forcedUpdate, or a tracked BeanCollection
with non-empty additions or removals). The !dirty guard in
preManyToManyUpdate() prevents double-firing when the bean itself is
also dirty.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 11:43:16 +12:00
Rob Bygraveandrobin.bygrave 38146fdce5 Support option to have always non-null @Embeddable(s) refereneces (#3825)
(#3702)

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 22:02:15 +12:00
Rob Bygraveandrobin.bygrave 864aff6c3b Implement query.exists() via select exists(...) (#3824)
* Implement query.exists() via select exists(...)

Previously query.exists() was implemented by running a findIds() limited to 1 row and checking whether the list was empty. This PR replaces that with a dedicated select exists(select 1 ...) execution path.

## Changes:

• CQueryExists — new query executor, analogous to CQueryCount, that runs select exists(...) and returns the boolean result directly from the JDBC ResultSet
• CQueryBuilder.buildExistsQuery() — builds select exists(select 1 ...) SQL, reusing the query plan cache on repeat calls
• CQueryEngine.findExists() / DefaultOrmQueryEngine / OrmQueryEngine interface — wire the new executor through the standard query engine stack, including SQL logging, summary logging, and query cache put
• DefaultServer.exists() — delegate to request.findExists() instead of findIds()

## Tests added to TestQueryExists:

• testExistsBoolean_returnsFalse — verifies false is returned when no rows match
• testExistsBoolean_withJoin — exercises the join SQL path in buildExistsQuery; also checks false when the join yields no match
• testExistsBoolean_queryPlanReuse — runs the same query twice and asserts the generated SQL is identical, confirming the plan cache is hit on the second call

* Fix TestQueryExists only

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 18:32:50 +12:00
Rob Bygraveandrobin.bygrave 24da374aa8 Fix #3821 fetchQuery secondary tables not included in query cache dep… (#3823)
* Fix #3821 fetchQuery secondary tables not included in query cache dependent tables

When a query uses fetchQuery("path"), the secondary table(s) must be registered as dependent tables in the query cache entry so that updates to those tables properly invalidate the cache.

## Fix:
In CQueryPlan, after building the SQL-tree dependent tables, walk the query's OrmQueryDetail for any fetchQuery paths and resolve each path through the root BeanDescriptor to its target entity's baseTable(). These tables are merged into dependentTables at plan construction time (once, then cached in the plan).

This means the fix applies uniformly whether paths were specified via fetchQuery("path") calls or via query.select(fetchGroup) — both populate the same OrmQueryDetail.

### Tests added to TestQueryCacheTableDependency:

• fetchQuery_oneToMany_invalidatesQueryCacheOnSecondaryTableUpdate — updating a contact invalidates a Customer query cache that includes fetchQuery("contacts"); asserts the refreshed result contains the updated phone number

• fetchQuery_manyToOne_invalidatesQueryCacheOnSecondaryTableUpdate — updating a root record invalidates an ECacheChild query cache that includes fetchQuery("root")

* Ensure tests cleanup their test data

* query.secondaryQuery() (called during prepareQuery()) removes fetchQuery paths from OrmQueryDetail before CQueryPlan is built. The existing buildDependentTables iterated detail.entries() looking for isQueryFetch() entries — but they were already gone.

Changes:

1. OrmQueryRequest.java — added secondaryQueries() getter to expose the already-stored SpiQuerySecondary field.
2. CQueryPlan.java — rewrote buildDependentTables to accept SpiQuerySecondary instead of OrmQueryDetail. It now iterates secondary.queryJoins() (the paths already removed from detail) and adds each path's base table to the dependent tables set.

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 17:44:10 +12:00
Rob Bygraveandrobin.bygrave 398848378d Fix asOf() queries bypassing bean cache isolation (#3820)
* Fix asOf() queries bypassing bean cache isolation

asOf() queries must never read from or write to the bean cache since the cache holds only current state. Previously, lazy loads and secondary queries spawned by an asOf parent could return stale  current data from the cache instead of the historical snapshot.

- DefaultServer.findId(): skip bean cache check when isAsOfQuery()
  so the findById fast-path doesn't return current state for temporal queries

- DLoadContext: force useBeanCache=CacheMode.OFF when asOf != null so hitCache=false for all lazy load batch cache checks, and propagate CacheMode.OFF to secondary (+query) joins

- LoadBeanRequest.configureQuery(): when !loadCache (parent context disabled cache), explicitly set CacheMode.OFF on the lazy-load SQL query — previously left at CacheMode.AUTO which caused cache reads/writes even when the parent had cache disabled

Fixes #3713

* Simplify asOf to CacheMode.OFF

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 09:06:36 +12:00
Rob Bygraveandrobin.bygrave 688eee532c Fix #3817 incorrect type deserialization for @DbJson field in generic superclass (#3819)
In createJsonObjectMapperType(), remove the instanceof DeployBeanProperty
branch that overrode ownerType() with getField().getDeclaringClass().

For a generic superclass B<T>, getDeclaringClass() returns B with T
unresolved, so Jackson treated the field as Object and deserialized to
LinkedHashMap. DeployBeanProperty.ownerType() already returns
desc.getBeanType() (the concrete subclass), which gives Jackson the full
supertype context needed to resolve T correctly.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 00:08:47 +12:00
Rob BygraveandIliya Ivanov 88d7b5e85a Support @ManyToOne inside @Embeddable for query predicates and joins (#3816)
* Make embeddable diggable

* WIP

* WIP

* WIP

* WIP

* WIP

* Support @ManyToOne inside @Embeddable for query predicates and joins

 Fix path resolution for @ManyToOne associations nested inside @Embeddable
 classes. Previously, a query like .where().eq("address.country.name", "NZ")
 on an entity with an @Embedded EAddr (containing @ManyToOne Country country)
 would throw PersistenceException: Embedded Property country.name not found.

 Changes:
 - BeanPropertyAssocOne.buildElPropertyValue: correctly resolve @ManyToOne
   paths inside embedded by extracting first segment, validating via
   embeddedPropsMap, then delegating deeper traversal to the overridden
   property; restore embedded flag on ElPropertyChain for scalar leaves
 - BeanPropertyAssoc.targetDescriptor(): lazy-init fix for override copies
   created by BeanEmbeddedMetaFactory (initialise() is not called on them)
 - BeanDescriptor.extraJoin(): restore !assocProp.isEmbedded() guard to
   prevent null-table ExtraJoin for composite keys and other embeddables
 - ElPropertyChainBuilder/ElPropertyChain: restore embedded field and prefix
   computation so alias resolution works correctly for paths through embedded
   (e.g. outer.datePeriod.date1 → prefix "outer" not "outer.datePeriod")
 - SqlTreeBuilder.buildExtraJoins: remove erroneous removeAll on
   orderByIncludes that broke Formula2 placeholder resolution and distinct
   on aggregation queries with many-side order-by joins
 - SqlTreeAlias.addJoin: fix indentation
 - Tests: add three test cases to TestEmbeddedManyToOne covering WHERE
   predicate through embedded FK column, through association property
   (requiring a JOIN), and combined with fetch

* Throw PersistenceException with unknown path

---------

Co-authored-by: Iliya Ivanov <i.ivanov@proforge.org>
2026-07-01 23:33:40 +12:00
Rob Bygraveandrobin.bygrave 985e3b12b1 Fix ON_CONFLICT_NOTHING cascade causing FK violation (#3712) (#3818)
When inserting a bean with InsertOptions.ON_CONFLICT_NOTHING that has cascade children (OneToMany / exported OneToOne), a unique constraint conflict caused the parent insert to be silently skipped (0 rows), but Ebean still cascaded and attempted to insert the children — resulting in a FK violation.

Changes

• BeanDescriptor.hasCascadeChildren() — returns true when the bean has save-cascade children (OneToMany or exported OneToOne) that hold a FK back to this bean.
• PersistRequestBean.setInsertOptions() — when ON_CONFLICT_NOTHING is used and the bean has cascade children, sets skipBatchForTopLevel = true so the parent INSERT executes immediately (non-batched). This ensures the row count is known before any cascade runs. Beans without cascade children are unaffected and continue to batch normally.
• PersistRequestBean.checkRowCount() — when the parent INSERT returns 0 rows under ON_CONFLICT_NOTHING, sets insertConflictSkipped = true and returns early, leaving the bean unmarked as loaded/persisted.
• DmlHandler.checkRowCount() — skips postExecute() when the insert was conflict-skipped.
• DefaultPersister.insert() — guards saveAssocMany() with !request.isInsertConflictSkipped(), preventing cascade saves when the parent was not actually inserted.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-01 23:32:02 +12:00
robin.bygrave 9acc555e8e Sqlite: Add Insert On Conflict support (#3770) 2026-07-01 21:58:16 +12:00
Rob Bygrave 73a646e7dc Merge pull request #3812 from dragkes/feature/invalidate-one-to-one-and-collection-cache
Invalidate one to one and collection cache (address #3811)
2026-07-01 21:21:19 +12:00
robin.bygrave 6d447a6c1a Add back the rollback() into CQueryBindCapture 2026-07-01 21:04:59 +12:00
robin.bygrave 16872fdb30 Add back the rollback() into CQueryBindCapture 2026-07-01 21:00:19 +12:00
robin.bygrave ec9303762e Fix the CQueryPlanManager conflict edit? 2026-07-01 20:50:34 +12:00