Skip to content

HIBERNATE-217: support StatelessSession.upsert on unversioned entities - #199

Open
jyemin wants to merge 2 commits into
mainfrom
HIBERNATE-217
Open

HIBERNATE-217: support StatelessSession.upsert on unversioned entities#199
jyemin wants to merge 2 commits into
mainfrom
HIBERNATE-217

Conversation

@jyemin

@jyemin jyemin commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Translate the OptionalTableUpdate built for StatelessSession.upsert and upsertMultiple into a single update statement: filter from the key bindings, $set over value bindings that are both insertable and updatable, $setOnInsert for insertable-only ones, upsert: true, multi: false.

visitOptionalTableUpdate serves two callers that hand it the same node: the boot-time forced version increment wants a plain update, the dialect wants an upsert. The caller's intent travels as the requested value descriptor, tested with expects(...).

The dialect translates eagerly at SessionFactory build time and defers every rejection to performMutation, since an eager throw would fail boot for entities that work today for everything except upsert: @Version entities throw TODO-HIBERNATE-216, attributes that are updatable but not insertable and entities whose only persistent attribute is the identifier throw bare messages (MQL has no matched-path-only write), and optional tables keep the TODO-HIBERNATE-69 rejection.

AstUpdateCommand now holds a list of {q, u, upsert?, multi} statements and renders byte-identically for every existing caller; AstDocumentUpdate gains $setOnInsert, rendering each operator only when non-empty and asserting the update is never empty; MongoStatement accepts upsert in update statements and extends the JDBC row count to matched plus upserted.

@jyemin jyemin self-assigned this Aug 1, 2026
@jyemin
jyemin requested a review from a team as a code owner August 1, 2026 20:23
@jyemin
jyemin requested review from strogiyotec and removed request for a team August 1, 2026 20:23
Base automatically changed from HIBERNATE-220 to main August 5, 2026 19:48
jyemin added 2 commits August 5, 2026 15:54
Translate the OptionalTableUpdate built for StatelessSession.upsert and
upsertMultiple into a single update statement: filter from the key
bindings, $set over value bindings that are both insertable and
updatable, $setOnInsert for insertable-only ones, upsert: true,
multi: false.

visitOptionalTableUpdate serves two callers that hand it the same node:
the boot-time forced version increment wants a plain update, the dialect
wants an upsert. The caller's intent travels as the requested value
descriptor, tested with expects(...).

The dialect translates eagerly at SessionFactory build time and defers
every rejection to performMutation, since an eager throw would fail boot
for entities that work today for everything except upsert: @Version
entities throw TODO-HIBERNATE-216, attributes that are updatable but not
insertable and entities whose only persistent attribute is the
identifier throw bare messages (MQL has no matched-path-only write), and
optional tables keep the TODO-HIBERNATE-69 rejection.

AstUpdateCommand now holds a list of {q, u, upsert?, multi} statements
and renders byte-identically for every existing caller; AstDocumentUpdate
gains $setOnInsert, rendering each operator only when non-empty and
asserting the update is never empty; MongoStatement accepts upsert in
update statements and extends the JDBC row count to matched plus
upserted.
Hibernate's merge coordinator decomposes an aggregate embeddable into one
binding per leaf field, so the upsert translation emitted one parameter per
leaf, named addr.city, addr.zipCode, and so on. UpdateCoordinatorStandard
binds a single value for the aggregate as a whole, under the aggregate's own
selection expression. No parameter carried that name, so every upsert of an
entity holding a @struct failed with UnknownParameterException, while insert,
update and stateful persist all succeeded for the same mapping.

Collapse each aggregate's leaf bindings into a single field update named after
the aggregate column, backed by one synthesized ColumnValueParameter carrying
the aggregate's mapping, so MongoStructJdbcType serializes the whole struct
exactly as it does on the standard update path. The leaves' value expressions
are deliberately not visited: each visit would append a parameter binder
without a matching placeholder and shift every later binding.

$set versus $setOnInsert is decided by the aggregate column's own updatable
flag rather than the leaves', since the aggregate is what the coordinator
binds.

The root cause is upstream, HHH-20754, and reproduces on PostgreSQL with no
MongoDB code involved. The collapsing code is labelled with that issue and
with the condition under which it becomes deletable.

Tests cover the shapes the collapsing logic branches on: two aggregates in one
entity, a struct containing an array, a struct with a @parent back-reference, a
null struct value, a struct mapped to a renamed column, the dot boundary that
keeps an aggregate distinct from a scalar whose column name shares its prefix,
and upsertMultiple over aggregates.
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.

1 participant