From e116e2a33fde5ef7b36c0863b3971c0a6f28b53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 20 Oct 2026 14:37:25 +0200 Subject: [PATCH 1/6] Java - draft - delta and delete --- java/working-with-cql/query-api.md | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/java/working-with-cql/query-api.md b/java/working-with-cql/query-api.md index b9472fe81a..175d168209 100644 --- a/java/working-with-cql/query-api.md +++ b/java/working-with-cql/query-api.md @@ -1340,6 +1340,8 @@ See the result of the updated *Order*: In **delta** representation, nested entity collections in the update data can be partial: the runtime only processes entities that are contained in the collection but entities that aren't contained remain untouched. Entities that shall be removed need to be included in the list and explicitly _marked for removal_. +##### Creating a Delta Representation + Using the same sample _Order_ as in the previous full-set chapter, do a deep delta update with the following update data: ```java @@ -1359,10 +1361,47 @@ order.setItems(delta(item1, item2.forRemoval(), item4)); Update.entity(ORDER).data(order); ``` -> Create delta collections via `CdsList` and `CdsData`. +> Create delta collections via `CdsList.delta()` and `CdsData.forRemoval()`. The deep update with order items in delta representation has similar effects as the update with items in full set representation. The only difference is that `OrderItem 3` is not deleted. +##### Processing a Delta Representation in a Custom Handler { #deep-update-delta-processing} + +Custom handlers registered on the `UPDATE` event may receive a delta payload — either from an explicit `CdsList.delta()` update statement, or from draft activation when [`cds.drafts.saveMode: delta`](../fiori-drafts#draft-delta-save-mode) is enabled. Such a handler needs to inspect the nested collections to distinguish delta lists from full-set lists, and to tell apart entries to be removed from entries to be created or updated. + +Two API methods are relevant: + +- [**`CdsList.isDelta()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsList.html#isDelta()) — Returns `true` if a nested collection is a delta list. To call it, cast the collection to `CdsList`. If the list is not a delta list, it is a full-set list and all pre-existing items not contained in it will be deleted. + +- [**`CdsData.isForRemoval()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsData.html#isForRemoval()) — Returns `true` if an individual entry in a delta list is marked for removal. Entries not marked for removal are to be created or updated. + +The following example shows how to handle both cases in a `@Before` handler: + +```java +@Before +public void beforeUpdateOrder(CdsUpdateEventContext context, List orders) { + for (Orders order : orders) { + List items = order.getItems(); + if (items instanceof CdsList cdsList && cdsList.isDelta()) { + // delta list — only the changed subset is present + for (OrderItem item : items) { + if (item.isForRemoval()) { + // item is marked for removal + } else { + // item is to be created or updated + } + } + } else if (items != null) { + // full-set list — pre-existing items not in this list will be deleted + } + } +} +``` + +::: warning Adapt handlers when using delta save mode +Handlers that assume the full document is always present must be adapted before enabling `cds.drafts.saveMode: delta`. Retain `cds.drafts.saveMode: full` (the default) if your handlers require the complete document. +::: + ### Bulk Update: Update Multiple Entity Records with Individual Data {#bulk-update} From 8793aa22167165ec510dda6b3fa691c9ad7d3d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 20 Oct 2026 14:38:09 +0200 Subject: [PATCH 2/6] Update fiori-drafts.md --- java/fiori-drafts.md | 65 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 618d4d773f..70f909c671 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -100,6 +100,48 @@ The `DRAFT_CREATE` is an internal event that is not triggered by OData requests For more examples, see the [Bookshop sample application](https://github.com/SAP-samples/cloud-cap-samples-java/tree/master/srv/src/main/java/my/bookshop/handlers/AdminServiceHandler.java). +## Deleting Drafts { #draft-deletion-in-cap-java } + +When a draft is deleted — whether by user cancellation, during activation, or by garbage collection — CAP Java removes the draft rows from the database. The deletion strategy is controlled via: + +```yaml +cds.drafts.deleteStrategy: flat +``` + +Three strategies are available: + +| Strategy | Behaviour | Performance | +|---|---|---| +| `flat` *(default)* | Deletes each draft table's rows individually by draft UUID. | Fast | +| `deep` | Issues a delete on the draft root; the runtime [cascades it along compositions](./working-with-cql/query-api#delete), removing child rows without explicit per-table statements. | Slow | +| `dad` | Deletes only the `DraftAdministrativeData` record and relies on database `ON DELETE CASCADE` constraints to remove the associated draft rows. | Fastest | + +### Cascade Deletion on Database { #draft-deletion-dad } + +The `dad` strategy avoids separate deletion operations for each draft table. Before enabling it, the database must have `ON DELETE CASCADE` constraints on each draft table's `DraftAdministrativeData_DraftUUID` foreign key. Generate these constraints by enabling: + +```yaml +cds.fiori.draft_admin_integrity: true +``` + +On SAP HANA and PostgreSQL, the constraints are deployed with `NOT VALIDATED` / `NOT VALID`, so existing data is not checked and no orphan cleanup is required before deploying. + +Once the constraints are deployed, switch to the `dad` strategy: + +```yaml +cds.drafts.deleteStrategy: dad +``` + +::: warning Check Custom Handlers +Switching strategies changes which event handlers are called during draft deletion. Verify the behavior of your custom handlers before adopting `dad`. +::: + +::: tip Retain `flat` Until Migration Is Complete +Keep `cds.drafts.deleteStrategy: flat` on existing databases until the cascade constraints have been deployed. +::: + +[Learn more about the shared migration requirements for CAP Node.js and CAP Java.](../releases/2026/oct26#optimized-draft-deletion){.learn-more} + ## Activating Drafts When you finish editing drafts by pressing the *Save* button, a draft gets activated. That means, either a single `CREATE` or `UPDATE` event is triggered to create or update the active entity with all of its compositions through a deeply structured document. You can register to these events to validate the activated data. @@ -116,6 +158,27 @@ 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 Save Mode { #draft-delta-save-mode } + +By default, draft activation sends the full document tree to the `CREATE` or `UPDATE` handler (`saveMode: full`). With delta save mode enabled, CAP Java performs a deep comparison of the draft against the active image and issues update operations only for entities that were actually changed — entities on an unchanged path are skipped entirely, resulting in more efficient database operations for large documents. + +Enable delta save mode via: + +```yaml +cds.drafts.saveMode: delta +``` + +Compared to the default `full` mode, the observable impact is: + +- **Update handlers** receive only the [delta representation](../working-with-cql/query-api#deep-update-delta) — the changed subtree — rather than the full document. Nested composition collections are delivered as delta lists. +- **Managed timestamps** (`modifiedAt`, `createdAt`) are updated only for nodes on the changed path, accurately reflecting which parts of the document were touched. + +::: warning Check Custom Update Handlers +Make sure your `UPDATE` handlers can deal with a delta payload. Handlers that assume they always receive the full document need to be adapted. Retain `cds.drafts.saveMode: full` (the default) if your handlers require the complete document. +::: + +[Learn more about processing delta payloads in a handler.](../working-with-cql/query-api#deep-update-delta-processing){.learn-more} + ## 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. @@ -212,7 +275,7 @@ cds.security.authorization.draftProtection.enabled: false ## Draft Garbage Collection { #draft-gc } -Stale drafts are automatically deleted after a timeout (30 days default). You can configure the timeout with the following application configuration property: +Stale drafts are automatically deleted after a timeout (30 days default). The [deletion strategy](#draft-deletion-in-cap-java) configured via `cds.drafts.deleteStrategy` also applies during garbage collection. You can configure the timeout with the following application configuration property: ```yaml cds.drafts.deletionTimeout: 8w From 5340e27b78155320e1b8529700654b236447c6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 22 Oct 2026 09:10:06 +0200 Subject: [PATCH 3/6] Update java/fiori-drafts.md Co-authored-by: Matthias Schur <107557548+MattSchur@users.noreply.github.com> --- 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 70f909c671..db370c8a66 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -118,7 +118,7 @@ Three strategies are available: ### Cascade Deletion on Database { #draft-deletion-dad } -The `dad` strategy avoids separate deletion operations for each draft table. Before enabling it, the database must have `ON DELETE CASCADE` constraints on each draft table's `DraftAdministrativeData_DraftUUID` foreign key. Generate these constraints by enabling: +The `dad` strategy avoids separate deletion operations for each draft table. Before enabling it, the database must have [ON DELETE CASCADE](../guides/databases/cdl-to-ddl#on-delete-cascade) constraints on each draft table's `DraftAdministrativeData_DraftUUID` foreign key. Generate these constraints by enabling: ```yaml cds.fiori.draft_admin_integrity: true From 6b431416b3c1c279dfca24256eb9c57ebc8cd89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 22 Oct 2026 09:18:34 +0200 Subject: [PATCH 4/6] fix links --- java/fiori-drafts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index db370c8a66..dff10e5117 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -170,14 +170,14 @@ cds.drafts.saveMode: delta Compared to the default `full` mode, the observable impact is: -- **Update handlers** receive only the [delta representation](../working-with-cql/query-api#deep-update-delta) — the changed subtree — rather than the full document. Nested composition collections are delivered as delta lists. +- **Update handlers** receive only the [delta representation](./working-with-cql/query-api#deep-update-delta) — the changed subtree — rather than the full document. Nested composition collections are delivered as delta lists. - **Managed timestamps** (`modifiedAt`, `createdAt`) are updated only for nodes on the changed path, accurately reflecting which parts of the document were touched. ::: warning Check Custom Update Handlers Make sure your `UPDATE` handlers can deal with a delta payload. Handlers that assume they always receive the full document need to be adapted. Retain `cds.drafts.saveMode: full` (the default) if your handlers require the complete document. ::: -[Learn more about processing delta payloads in a handler.](../working-with-cql/query-api#deep-update-delta-processing){.learn-more} +[Learn more about processing delta payloads in a handler.](./working-with-cql/query-api#deep-update-delta-processing){.learn-more} ## Read-Only Fields in Drafts { #readonly-in-drafts } From e73c53b65c6346ded3106859ebc1718776405463 Mon Sep 17 00:00:00 2001 From: Mahati Shankar <93712176+smahati@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:13:49 +0200 Subject: [PATCH 5/6] cosmetics --- java/working-with-cql/query-api.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/working-with-cql/query-api.md b/java/working-with-cql/query-api.md index 175d168209..eb82859949 100644 --- a/java/working-with-cql/query-api.md +++ b/java/working-with-cql/query-api.md @@ -1367,13 +1367,13 @@ The deep update with order items in delta representation has similar effects as ##### Processing a Delta Representation in a Custom Handler { #deep-update-delta-processing} -Custom handlers registered on the `UPDATE` event may receive a delta payload — either from an explicit `CdsList.delta()` update statement, or from draft activation when [`cds.drafts.saveMode: delta`](../fiori-drafts#draft-delta-save-mode) is enabled. Such a handler needs to inspect the nested collections to distinguish delta lists from full-set lists, and to tell apart entries to be removed from entries to be created or updated. +Custom handlers registered on the `UPDATE` event receive a delta payload when there is an explicit `CdsList.delta()` update statement, or from draft activation when [`cds.drafts.saveMode: delta`](../fiori-drafts#draft-delta-save-mode) is enabled. Such a handler needs to inspect the nested collections to distinguish delta lists from full-set lists, and to tell apart entries to be removed from entries to be created or updated. Two API methods are relevant: -- [**`CdsList.isDelta()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsList.html#isDelta()) — Returns `true` if a nested collection is a delta list. To call it, cast the collection to `CdsList`. If the list is not a delta list, it is a full-set list and all pre-existing items not contained in it will be deleted. +- [**`CdsList.isDelta()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsList.html#isDelta()): Returns `true` if a nested collection is a delta list. To call it, cast the collection to `CdsList`. If the list isn't a delta list, it's a full-set list and all pre-existing items not contained in it are deleted. -- [**`CdsData.isForRemoval()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsData.html#isForRemoval()) — Returns `true` if an individual entry in a delta list is marked for removal. Entries not marked for removal are to be created or updated. +- [**`CdsData.isForRemoval()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsData.html#isForRemoval()): Returns `true` if an individual entry in a delta list is marked for removal. Entries not marked for removal are to be created or updated. The following example shows how to handle both cases in a `@Before` handler: @@ -1383,7 +1383,7 @@ public void beforeUpdateOrder(CdsUpdateEventContext context, List orders for (Orders order : orders) { List items = order.getItems(); if (items instanceof CdsList cdsList && cdsList.isDelta()) { - // delta list — only the changed subset is present + // delta list: only the changed subset is present for (OrderItem item : items) { if (item.isForRemoval()) { // item is marked for removal @@ -1392,7 +1392,7 @@ public void beforeUpdateOrder(CdsUpdateEventContext context, List orders } } } else if (items != null) { - // full-set list — pre-existing items not in this list will be deleted + // full-set list: pre-existing items not in this list will be deleted } } } From 84cc70dfd746e47ebc1a13b358bcd99ba1aac795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeglinsky?= Date: Fri, 23 Oct 2026 11:26:09 +0200 Subject: [PATCH 6/6] Apply batched suggestions from code review Co-authored-by: hyperspace-pr-bot[bot] <209611008+hyperspace-pr-bot[bot]@users.noreply.github.com> --- 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 dff10e5117..daf6cc0ceb 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -102,7 +102,7 @@ For more examples, see the [Bookshop sample application](https://github.com/SAP- ## Deleting Drafts { #draft-deletion-in-cap-java } -When a draft is deleted — whether by user cancellation, during activation, or by garbage collection — CAP Java removes the draft rows from the database. The deletion strategy is controlled via: +When a draft is deleted through user cancellation, activation, or garbage collection, CAP Java removes the draft rows from the database. Configure the deletion strategy with the following property: ```yaml cds.drafts.deleteStrategy: flat @@ -110,10 +110,10 @@ cds.drafts.deleteStrategy: flat Three strategies are available: -| Strategy | Behaviour | Performance | +| Strategy | Behavior | Performance | |---|---|---| | `flat` *(default)* | Deletes each draft table's rows individually by draft UUID. | Fast | -| `deep` | Issues a delete on the draft root; the runtime [cascades it along compositions](./working-with-cql/query-api#delete), removing child rows without explicit per-table statements. | Slow | +| `deep` | Issues a delete on the draft root. The runtime [cascades the deletion along compositions](./working-with-cql/query-api#delete) and removes child rows without explicit per-table statements. | Slow | | `dad` | Deletes only the `DraftAdministrativeData` record and relies on database `ON DELETE CASCADE` constraints to remove the associated draft rows. | Fastest | ### Cascade Deletion on Database { #draft-deletion-dad } @@ -124,7 +124,7 @@ The `dad` strategy avoids separate deletion operations for each draft table. Bef cds.fiori.draft_admin_integrity: true ``` -On SAP HANA and PostgreSQL, the constraints are deployed with `NOT VALIDATED` / `NOT VALID`, so existing data is not checked and no orphan cleanup is required before deploying. +On SAP HANA, the constraints are deployed with `NOT VALIDATED`. On PostgreSQL, they are deployed with `NOT VALID`. These options skip checks of existing data, so you do not need to clean up orphaned data before deployment. Once the constraints are deployed, switch to the `dad` strategy: @@ -160,7 +160,7 @@ In case the create or update operation raises an error, the transaction is rolle ### Delta Save Mode { #draft-delta-save-mode } -By default, draft activation sends the full document tree to the `CREATE` or `UPDATE` handler (`saveMode: full`). With delta save mode enabled, CAP Java performs a deep comparison of the draft against the active image and issues update operations only for entities that were actually changed — entities on an unchanged path are skipped entirely, resulting in more efficient database operations for large documents. +By default, draft activation sends the full document tree to the `CREATE` or `UPDATE` handler (`saveMode: full`). With delta save mode enabled, CAP Java compares the draft with the active image. It updates only entities on changed paths and skips entities on unchanged paths. This behavior reduces database operations for large documents. Enable delta save mode via: