From d674feca95a6dcf8f8881663d1030595014b997d Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Thu, 8 Oct 2026 09:18:26 +0200 Subject: [PATCH 1/5] Document delta draft save mode and getDelta() --- java/fiori-drafts.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 618d4d773f..1a63060dca 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -116,6 +116,58 @@ public void validateOrderItem(CdsCreateEventContext context, OrderItems orderIte During activation the draft data is deleted from the database. This happens before the active entity is created or updated within the same transaction. In case the create or update operation raises an error, the transaction is rolled back and the draft data is restored. +### Delta Draft Activation { #delta-draft } + +By default, when a draft is activated, the entire draft document — including every composition child at every level — is written back to the active tables unconditionally. This *full* mode is simple and safe, but it means that every child entity receives a new `modifiedAt` timestamp even when nothing about it actually changed. + +*Delta* mode changes this: before writing, the draft image is compared to the current active image. Only entities on a *changed path* — entities that have changed fields, or that are ancestors of added or removed children — are included in the database update. Unchanged siblings are simply left out and their `modifiedAt` stays at its previous value. + +Enable delta mode with the `cds.drafts.save-mode` property: + +```yaml +cds: + drafts: + save-mode: delta +``` + +#### Effect on the database + +In delta mode, a single deep `UPDATE` statement is issued for the changed subgraph of the document. The following rules determine which rows are touched: + +- **Changed node**: a row whose own scalar fields differ from the active image is included. Managed fields (`@cds.on.update`, for example `modifiedAt`) are stamped fresh on every row included in the update. +- **Ancestor on a changed path**: if a child node changes, its parent is also included in the deep update, all the way up to the root. These ancestors therefore also receive a new `modifiedAt`, even if their own scalar fields did not change. +- **Unchanged sibling**: a child node whose own fields and sub-tree are identical to the active image is *absent* from the update payload. Its database row is not touched and its `modifiedAt` remains unchanged. +- **Added child**: a newly added composition child is inserted as a full new row. +- **Removed child**: a composition child that was deleted from the draft is removed from the active table. + +Consider a three-level document `Travel → Booking → BookingSupplement`. When only `Booking 1.price` is patched: + +- `Booking 1` is updated (changed fields, `modifiedAt` advances). +- `Travel` is updated (ancestor on the changed path, `modifiedAt` advances). +- `Booking 2` (sibling of `Booking 1`) is **not** updated — absent from the payload, `modifiedAt` unchanged. +- All `BookingSupplements` under `Booking 1` are **not** updated — no changes in their sub-tree, `modifiedAt` unchanged. + +If the active counterpart of a draft root cannot be found — for example because the active record was deleted externally between `draftEdit` and `draftActivate` — delta mode falls back to a full update for that root, re-inserting it as a new entity. + +Activating a draft that has no changes at all is idempotent: the update payload contains only the key fields of the root, no composition children are written, and no `modifiedAt` values advance. + +#### Effect on event handlers + +From a handler's perspective, delta mode is transparent: the same `CREATE` and `UPDATE` events fire as in full mode, and the `DraftSaveEventContext.getResult()` returns the same flat root row (keys plus draft virtual fields such as `IsActiveEntity`). Composition children are stripped from the result by the framework, as in full mode — the OData adapter re-reads the full document to build the response sent to the UI. + +```java +@After(event = DraftService.EVENT_DRAFT_SAVE) +public void afterSave(DraftSaveEventContext context) { + // context.getResult() contains a flat root row with IsActiveEntity = true. + // Composition children are absent from the result in both full and delta mode. + context.getResult().forEach(row -> { /* ... */ }); +} +``` + +The key difference is that in delta mode, the `UPDATE` event receives only the changed subgraph as its payload. Custom `@Before` or `@On` handlers that inspect `context.getCqn()` will see a deeply structured update restricted to the changed nodes, not the full document. `@After` handlers on the individual `CREATE` / `UPDATE` events see only the rows that were actually written. + +In delta mode, `DraftSaveEventContext.getDelta()` provides the changed subgraph as a `Result` — the same deeply structured rows that were written, including composition children. Ancestors on the changed path are included even if their own scalar fields did not change (managed-field stamping makes them non-NOP). Unchanged siblings absent from the update are not present. `getDelta()` returns `null` in full mode. + ## Read-Only Fields in Drafts { #readonly-in-drafts } By default, `@readonly` and `@Core.Computed` fields are only enforced when a draft is activated, that means during the `CREATE` or `UPDATE` event on the active entity. Until then, such fields can still be changed on the draft, for example through an OData `PATCH` request. From 7c732135d623db150c42754a1d66850be5a4dcf3 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Thu, 8 Oct 2026 09:21:21 +0200 Subject: [PATCH 2/5] Flag delta draft activation as beta --- java/fiori-drafts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 1a63060dca..9badf0cbd2 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -116,7 +116,7 @@ public void validateOrderItem(CdsCreateEventContext context, OrderItems orderIte During activation the draft data is deleted from the database. This happens before the active entity is created or updated within the same transaction. In case the create or update operation raises an error, the transaction is rolled back and the draft data is restored. -### Delta Draft Activation { #delta-draft } +### Delta Draft Activation { #delta-draft } By default, when a draft is activated, the entire draft document — including every composition child at every level — is written back to the active tables unconditionally. This *full* mode is simple and safe, but it means that every child entity receives a new `modifiedAt` timestamp even when nothing about it actually changed. From f13947beb764ae5c7be0c84151b83d73e78f75d6 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Thu, 8 Oct 2026 09:22:59 +0200 Subject: [PATCH 3/5] Compact effect-on-database section for delta draft --- java/fiori-drafts.md | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 9badf0cbd2..6f6bb6ac3f 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -132,24 +132,11 @@ cds: #### Effect on the database -In delta mode, a single deep `UPDATE` statement is issued for the changed subgraph of the document. The following rules determine which rows are touched: +Only entities on a *changed path* are written: a changed node itself, and all its ancestors up to the root. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Added children are inserted, removed children are deleted. Managed fields (`modifiedAt` etc.) are stamped on every node that is written. -- **Changed node**: a row whose own scalar fields differ from the active image is included. Managed fields (`@cds.on.update`, for example `modifiedAt`) are stamped fresh on every row included in the update. -- **Ancestor on a changed path**: if a child node changes, its parent is also included in the deep update, all the way up to the root. These ancestors therefore also receive a new `modifiedAt`, even if their own scalar fields did not change. -- **Unchanged sibling**: a child node whose own fields and sub-tree are identical to the active image is *absent* from the update payload. Its database row is not touched and its `modifiedAt` remains unchanged. -- **Added child**: a newly added composition child is inserted as a full new row. -- **Removed child**: a composition child that was deleted from the draft is removed from the active table. +For example, in a `Travel → Booking → BookingSupplement` document, patching `Booking 1.price` updates `Booking 1` and `Travel`, but leaves `Booking 2` and all `BookingSupplements` untouched. -Consider a three-level document `Travel → Booking → BookingSupplement`. When only `Booking 1.price` is patched: - -- `Booking 1` is updated (changed fields, `modifiedAt` advances). -- `Travel` is updated (ancestor on the changed path, `modifiedAt` advances). -- `Booking 2` (sibling of `Booking 1`) is **not** updated — absent from the payload, `modifiedAt` unchanged. -- All `BookingSupplements` under `Booking 1` are **not** updated — no changes in their sub-tree, `modifiedAt` unchanged. - -If the active counterpart of a draft root cannot be found — for example because the active record was deleted externally between `draftEdit` and `draftActivate` — delta mode falls back to a full update for that root, re-inserting it as a new entity. - -Activating a draft that has no changes at all is idempotent: the update payload contains only the key fields of the root, no composition children are written, and no `modifiedAt` values advance. +If the active counterpart of a draft root cannot be found at activation time, delta mode falls back to a full insert for that root. #### Effect on event handlers From 2898db0fc339d436aef0d1f070f6364264ab65cb Mon Sep 17 00:00:00 2001 From: Mahati Shankar <93712176+smahati@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:01:09 +0200 Subject: [PATCH 4/5] cosmetics --- java/fiori-drafts.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 6f6bb6ac3f..3bd5f15747 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -120,7 +120,7 @@ In case the create or update operation raises an error, the transaction is rolle By default, when a draft is activated, the entire draft document — including every composition child at every level — is written back to the active tables unconditionally. This *full* mode is simple and safe, but it means that every child entity receives a new `modifiedAt` timestamp even when nothing about it actually changed. -*Delta* mode changes this: before writing, the draft image is compared to the current active image. Only entities on a *changed path* — entities that have changed fields, or that are ancestors of added or removed children — are included in the database update. Unchanged siblings are simply left out and their `modifiedAt` stays at its previous value. +*Delta* mode changes the activation process. Before writing, the draft image is compared to the current active image. Only entities on a *changed path* — entities that have changed fields, or that are ancestors of added or removed children — are included in the database update. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Enable delta mode with the `cds.drafts.save-mode` property: @@ -132,11 +132,11 @@ cds: #### Effect on the database -Only entities on a *changed path* are written: a changed node itself, and all its ancestors up to the root. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Added children are inserted, removed children are deleted. Managed fields (`modifiedAt` etc.) are stamped on every node that is written. +Only entities on a *changed path* are written. These entities include the changed node itself, and all its ancestors up to the root. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Added children are inserted and removed children are deleted. Managed fields (Like `modifiedAt`) are stamped on every node that is written. For example, in a `Travel → Booking → BookingSupplement` document, patching `Booking 1.price` updates `Booking 1` and `Travel`, but leaves `Booking 2` and all `BookingSupplements` untouched. -If the active counterpart of a draft root cannot be found at activation time, delta mode falls back to a full insert for that root. +If the active counterpart of a draft root can't be found at activation time, delta mode falls back to a full insert for that root. #### Effect on event handlers @@ -151,9 +151,9 @@ public void afterSave(DraftSaveEventContext context) { } ``` -The key difference is that in delta mode, the `UPDATE` event receives only the changed subgraph as its payload. Custom `@Before` or `@On` handlers that inspect `context.getCqn()` will see a deeply structured update restricted to the changed nodes, not the full document. `@After` handlers on the individual `CREATE` / `UPDATE` events see only the rows that were actually written. +The key difference is that in delta mode, the `UPDATE` event receives only the changed subgraph as its payload. Your custom `@Before` or `@On` handlers that inspect `context.getCqn()` sees a deeply structured update restricted to the changed nodes, not the full document. `@After` handlers on the individual `CREATE` / `UPDATE` events see only the rows that were written. -In delta mode, `DraftSaveEventContext.getDelta()` provides the changed subgraph as a `Result` — the same deeply structured rows that were written, including composition children. Ancestors on the changed path are included even if their own scalar fields did not change (managed-field stamping makes them non-NOP). Unchanged siblings absent from the update are not present. `getDelta()` returns `null` in full mode. +In delta mode, `DraftSaveEventContext.getDelta()` provides the changed subgraph as a `Result` — the same deeply structured rows that were written, including composition children. Ancestors on the changed path are included even if their own scalar fields didn't change (managed-field stamping makes them non-NOP). Unchanged siblings absent from the update aren't present. `getDelta()` returns `null` in full mode. ## Read-Only Fields in Drafts { #readonly-in-drafts } From 97c2b39dfb5b2bf5f72d2cbd88cac34184311dff Mon Sep 17 00:00:00 2001 From: Mahati Shankar <93712176+smahati@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:17:34 +0200 Subject: [PATCH 5/5] cosmetics --- java/fiori-drafts.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 3bd5f15747..8819099be6 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -118,9 +118,9 @@ In case the create or update operation raises an error, the transaction is rolle ### Delta Draft Activation { #delta-draft } -By default, when a draft is activated, the entire draft document — including every composition child at every level — is written back to the active tables unconditionally. This *full* mode is simple and safe, but it means that every child entity receives a new `modifiedAt` timestamp even when nothing about it actually changed. +By default, when a draft is activated, the entire draft document, including every composition child at every level, is written back to the active tables unconditionally. This *full* mode is simple and safe, but it means that every child entity receives a new `modifiedAt` timestamp even when nothing about it actually changed. -*Delta* mode changes the activation process. Before writing, the draft image is compared to the current active image. Only entities on a *changed path* — entities that have changed fields, or that are ancestors of added or removed children — are included in the database update. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. +*Delta* mode changes the activation process. Before writing, the draft image is compared to the current active image. Only entities on a *changed path*, that is, entities that have changed fields or that are ancestors of added or removed children, are included in the database update. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Enable delta mode with the `cds.drafts.save-mode` property: @@ -130,7 +130,7 @@ cds: save-mode: delta ``` -#### Effect on the database +#### Effect on the Database Only entities on a *changed path* are written. These entities include the changed node itself, and all its ancestors up to the root. Unchanged siblings are left out and their `modifiedAt` stays at its previous value. Added children are inserted and removed children are deleted. Managed fields (Like `modifiedAt`) are stamped on every node that is written. @@ -138,9 +138,9 @@ For example, in a `Travel → Booking → BookingSupplement` document, patching If the active counterpart of a draft root can't be found at activation time, delta mode falls back to a full insert for that root. -#### Effect on event handlers +#### Effect on Event Handlers -From a handler's perspective, delta mode is transparent: the same `CREATE` and `UPDATE` events fire as in full mode, and the `DraftSaveEventContext.getResult()` returns the same flat root row (keys plus draft virtual fields such as `IsActiveEntity`). Composition children are stripped from the result by the framework, as in full mode — the OData adapter re-reads the full document to build the response sent to the UI. +From a handler's perspective, delta mode is transparent: the same `CREATE` and `UPDATE` events fire as in full mode, and the `DraftSaveEventContext.getResult()` returns the same flat root row (keys plus draft virtual fields such as `IsActiveEntity`). Composition children are stripped from the result by the framework, as in full mode, the OData adapter re-reads the full document to build the response sent to the UI. ```java @After(event = DraftService.EVENT_DRAFT_SAVE) @@ -153,7 +153,7 @@ public void afterSave(DraftSaveEventContext context) { The key difference is that in delta mode, the `UPDATE` event receives only the changed subgraph as its payload. Your custom `@Before` or `@On` handlers that inspect `context.getCqn()` sees a deeply structured update restricted to the changed nodes, not the full document. `@After` handlers on the individual `CREATE` / `UPDATE` events see only the rows that were written. -In delta mode, `DraftSaveEventContext.getDelta()` provides the changed subgraph as a `Result` — the same deeply structured rows that were written, including composition children. Ancestors on the changed path are included even if their own scalar fields didn't change (managed-field stamping makes them non-NOP). Unchanged siblings absent from the update aren't present. `getDelta()` returns `null` in full mode. +In delta mode, `DraftSaveEventContext.getDelta()` provides the changed subgraph as a `Result`, the same deeply structured rows that were written, including composition children. Ancestors on the changed path are included even if their own scalar fields didn't change (due to updates made by managed-field stamping). Unchanged siblings absent from the update aren't present. `getDelta()` returns `null` in full mode. ## Read-Only Fields in Drafts { #readonly-in-drafts }