Migrating to cds 10
This guide covers CAP Node.js packages, that is, @sap/cds* and @cap-js/* packages.
See also Migration Guides for CAP Java
CDS – Improved Checks
Annotations Without Targets
Annotations with invalid targets are generally reported as warnings by the cds compiler. However, in case of the security-related annotations, including @restrict, @requires, and @ams.*, this is not just a harmless issue, but may cause unauthorized access to be granted silently. To prevent such issues from being overlooked, we fixed the compiler to report such cases as errors instead of warnings.
Examples:
annotate AdmnService with @requires:'admin'; // typo: missing 'i'
annotate AdmnService.Book with @restrict: [...]; // typo: missing 's'
annotate AdminService.Books:tile with @ams.attributes: {...}; // missing 't'Are you affected?
Try compiling your CDS models to provoke any errors as shown below:
cds compile \*[ERROR] Artifact “AdmnService” has not been found (in annotate:“AdmnService”)
[ERROR] Artifact “AdmnService.Books” has not been found (in annotate:“AdmnService.Books”)
[ERROR] Element “ttle” has not been found (in annotate:Books/element:“ttle”)How to fix
If you encounter such errors, fix the typos in your annotations, or remove them if they are no longer needed.
Invalid Defaults for Structs
Before cds10, you could provide invalid default values for structured elements, which were silently ignored. With cds10, such invalid defaults result in an error. For example, the below was accepted but ignored in 2sql and 4odata backends:
type struct { a: Integer; b: String; }
entity Foo { bar: struct default 22; }Are you affected?
Try compiling your CDS models to provoke any errors as shown below:
cds compile \*[ERROR] Unexpected ‘default’ for a structured element with not exactly one sub element (in element: “bar”)How to fix?
If you get such an error, remove the invalid default. This has no negative impact on your application's behavior, as it was previously ignored.
Duplicate Elements
Before cds10, it was possible to extend an entity with multiple aspects that contain elements with the same name, leading to unexpected behavior. With cds10, this is now an error, to avoid such late surprises. For example:
entity E { ID : Integer; }
extend E with { field : String; };
extend E with { field : Date; };Are you affected?
Try compiling your CDS models to provoke any errors as shown below:
cds compile \*[ERROR] Duplicate definition of element “field” ...How to fix?
If you encounter such errors, you need to adapt your model to avoid duplicate elements. For example, you could simply remove one of the conflicting elements, or rename it.
Potentially Breaking Fixes
| Flag / Kill Switch | Details | Before | Now | Since |
|---|---|---|---|---|
| bypass_draft | Bypass Draft Choreography | false | true | Dec 23 |
| ieee754compatible | Decimals & Int64 as Strings | false | true | Jun 24 |
| decimal_affinity | Fixed Affinity for Decimals | "numeric" | "real" | Jun 26 |
| legacy_srv_results legacy_db_results | Fixed Service Results | true true | false true | Jun 26 |
| compat_srv_getters compat_texts_entities | Fixed srv.entities() | true true | false false | Dec 25 |
| compat_clone_appends | Fixed cds.ql.clone() | false | Jun 26 | |
| bulk_inserts_via_rest | Fixed Bulk Inserts via REST | true | Jun 26 |
Decimals & Int64 as Strings
Decimal and Int64 values cannot be represented as JavaScript numbers without risks of losing precision. Therefore many database drivers, including those for SAP HANA and PostgreSQL, always return such data as strings, while SQLite drivers return numbers.
This database-dependent discrepancy caused surprises for CAP projects when moving to production with SAP HANA or PostgreSQL after developing with SQLite.
To avoid such late surprises we consolidated the default behavior for SQLite with the behavior of SAP HANA and PostgreSQL. This is controlled by config option cds.features.ieee754compatible: true (was false before).
See also...
0.1 + 0.2 //> 0.30000000000000004
(0.1).toString(2)
(0.2).toString(2)
(0.3).toString(2)
(0.1 + 0.2).toString(2)(0.5 + 0.25 + 0.125).toString(2)
(1/2).toString(2)
(1/2/2).toString(2)
(1/2/2/2).toString(2)Yes, JavaScript has bigint support, but JSON doesn't. Even if CAP would custom-serialize them as numbers losslessly, clients parse these into JavaScript Numbers, losing precision. The same applies to Java, which has dedicated BigDecimal and BigInteger types, but these are not supported by JSON or JSON clients. Bottom line: If you want to exchange such data, always do so as strings.
Are you affected?
You are affected by this change...
Only in development with SQLite (no change with SAP HANA or PostgreSQL)
If you have
DecimalorInt64elements in your model. For example, search for usages of these types in your *.cds files like so:shellgrep -rni --exclude="*/node_modules/*" --include \*.cds ":\s*Decimal" grep -rni --exclude="*/node_modules/*" --include \*.cds ":\s*Int64"And you have custom code that does calculations with such fields, for example:
jsawait INSERT.into(Books).entries({ID:1,stock:10}) let { stock } = await SELECT.one.from (Books,1) stock = stock + 1 //> with SQLite: 11, with HANA: '101'And/or you have tests that compare such elements by equality, for example:
jsexpect(book.stock).to.equal(10) expect(book).to.equal({ ..., stock:10 })
You are not affected if:
- You already had to fix those discrepancies when you went productive before.
- You already switched on cds.features.ieee754compatible: true in the past.
- Even if you are affected, such string concatenations were ticking time bombs.
How to address?
Rewrite failing tests to avoid checking for strict numeric equality:
jsexpect(book.stock).to.equal('10') expect(book).to.equal({ ..., stock:'10' })If you need to do arithmetic in JavaScript, convert the data to a Number first:
jsstock = Number(stock) + 1Use
Doubleinstead ofDecimalif you can accept negligible precision loss.If you don't need functional correctness, for example, for prototypes or demos, revert to the former behavior by setting cds.features.ieee754compatible: false.
Tip
Best is to entirely avoid calculations with such fields in JavaScript, as they can always result in precision loss. Do them in the database instead. For example, this is safe: UPDATE Books set stock = stock + 1.
Bypass Drafts by Default
With cds10, direct access to active entities is allowed by default. Previously, this required opting in via cds.fiori.bypass_draft=true.
Beware of partial requests
While this change is not breaking, check whether your application's validation logic correctly handles the additional entry points. In particular, partial CREATE or UPDATE requests to root entities and their composition children are now possible, for example:
PATCH /Orders(...) { status: 'C' } // partial update on root entity
PATCH /Orders(...)/items(...) // partial update to nested items
POST /Orders(...)/items {...} // partial create of nested itemsOpt-out with kill switch
If you're unsure whether your application can handle the new behavior, you can opt-out from it and restore the former behavior by setting cds.fiori.bypass_draft: false.
Declarative constraints are safe
If you used declarative constraints for input validation, that is, via @assert: (...), your application is already safe, as these constraints are automatically applied to all entry points, including the new ones.
Fixed Service Results
Before cds10, results of local service calls involving write operations — that is, INSERT, UPDATE, and DELETE — were undocumented and inconsistent. Sometimes the result was the number of affected rows, sometimes the input data, and sometimes an object with the affectedRows property indicating the affected rows.
With cds10, we fixed and consolidated this behavior as follows:
All write operations of app services return an (array) object with property
affectedindicating the affected rows, for example, like that:jslet { affected } = await srv.create(Books).entries(...) let { affected } = await srv.update(Books) .where `stock > 111` ... let { affected } = await srv.delete(Books) .where `stock = 0`Same for db services, with opt-in via cds.features.legacy_db_results: false for a grace period, will become the default behavior in a future release.
The change to real array objects, also for InsertResults, lets you use object spread destructuring and standard array operations, for example, to retrieve generated primary keys from INSERTs:
let [ Emily, Charlotte ] = await srv.create(Authors).entries(
{name:'Emily Brontee'},
{name:'Charlotte Brontee'}
)This also allows support for SQL returning clauses in future with INSERT, UPDATE, and DELETE requests.
No change was made to the results of read operations, that is, SELECTs, which return plain arrays of entries, as before.
Are you affected?
You are affected by this change if you have custom code that relies on the former inconsistent and undocumented results of UPDATE and DELETE operations, in particular if you have...
- Calls to UPDATE or DELETE with app services which do expect
req.dataas result - Custom
afterhandlers for the same which do expectreq.dataas first argument - Calls to UPDATE or DELETE with db services which do expect a number as result
- Tests which expect an object, but not an array, as result of INSERTs on db level
How to address?
If you are affected, you should adapt your code to the new consistent results.
For cases 1 and 2, access the input data via req.data instead of the result, for example:
this.on ('UPDATE', Books, async (req, next) => {
let { ID, stock } = await next() // returned req.data before
await next(); let { ID, stock } = req.data // just access req.data explicitly now
})For case 3, access the number of affected rows via the affected property of the result:
let affected = await srv.update(Books) .where `stock > 111` ...
let { affected } = await srv.update(Books) .where `stock > 111` ...For case 4, adapt your tests to expect an array instead of an object, for example:
expect(result).to.deep.equal ({ affectedRows:1 })
expect({...result}).to.deep.equal ({ affectedRows:1 })
expect({...result}).to.deep.equal ({ affected:1 })
expect(result).to.have.property ('affected',1) Opt-in & Kill Switches
As a last resort, restore the former behavior with the following config options:
cds.features.... | -> restores: |
|---|---|
| legacy_srv_results: true | the undocumented former behavior |
| legacy_db_results: true | the former behavior, and still the default for cds10 |
Warning
As such kill switches restore unintended, and erroneous behavior, they should only be used as a temporary measure. It's recommended to update your code to be compatible with the new behavior. The kill switches will be removed in a future release, and relying on them for longer time may cause maintenance issues and technical debt.
Fixed srv.entities()
Previously, you could call the convenient shortcuts srv.entities, .types, .events, and .actions as a function. Moreover, the results returned by srv.entities, as well as cds.entities accidentally included compiler-generated *.texts entities. Both were undocumented and unintended, and are now fixed with cds10.
Are you affected?
You are affected by these fixes if you have custom code that relies on the former undocumented and unintended behavior. In particular, you can scan your code for the following patterns to check whether you are affected:
grep -rni --exclude="*/node_modules/*" --include \*.js ".entities\s*(" | grep -v cds.entities
grep -rni --exclude="*/node_modules/*" --include \*.js ".events\s*("
grep -rni --exclude="*/node_modules/*" --include \*.js ".types\s*("
grep -rni --exclude="*/node_modules/*" --include \*.js ".actions\s*("
grep -rni --exclude="*/node_modules/*" --include \*.js ".texts.*.entities"While these scans provide a good first approximation, they cannot be accurate. You may also have used
srv.entitiesin other ways, not matched by these scans.
How to address?
If you used the function variant, fix your code as follows:
const { Books } = srv.entities()
const { Books } = srv.entities ('sap.capire.bookshop') const { Books } = srv.entities // use getter, not as function call
const { Books } = cds.entities ('sap.capire.bookshop') // use cds.entitiesIf you relied on *.texts entities in the returned results, use the texts property of the respective primary entity instead:
const { "Books.texts": Books_texts } = srv.entities const { Books } = srv.entities
Books.texts //> use .texts property to access generated `*.texts` entitiesKill Switches
cds.features.... | -> restores: |
|---|---|
| compat_srv_getters: true | srv.entities() as a function |
| compat_texts_entities: true | *.texts entries in results of srv.entities |
Fixed cds.ql.clone()
We fixed a bug in the implementation of cds.ql.clone() which caused that Fluent API methods .columns(), .orderBy(), or .groupBy() did not append to existing clauses, as intended, but replaced instead.
Following shows the erroneous behavior (in red), and the fixed one (in green):
let q1 = SELECT`a,b`.from`Foo`.where`x>1`.orderBy`a`
let q2 = cds.ql.clone(q1)q2.columns`c`.where`y<2`.orderBy`b`
q1.columns`c`.where`y<2`.orderBy`b`q2 ⇒ SELECT c from Foo where x>1 and y<2 order by b -- was wrong
q2 ⇒ SELECT a, b, c from Foo where x>1 and y<2 order by a, b -- now fixed
q1 ⇒ SELECT a, b, c from Foo where x>1 and y<2 order by a, b -- as expectedAre you affected?
The likelihood that you are affected is low, as cds.ql.clone() was rolled out in January 26, 2026 and this only applies to a specific combination of API usages, which silently yielded wrong outcomes. You are only affected if all three of the following are true:
Are you using
cds.ql.clone()at all?shgrep -rnw --exclude="*/node_modules/*" --include="*.js" "ql.clone"You modified these using fluent API methods
.columns(),.orderBy(), or.groupBy()shgrep -rnw --exclude="*/node_modules/*" --include="*.js" ".columns" grep -rnw --exclude="*/node_modules/*" --include="*.js" ".orderBy" grep -rnw --exclude="*/node_modules/*" --include="*.js" ".groupBy"You relied on the erroneous behavior where these methods replaced existing clauses instead of appending to them.
How to address?
If you are affected, explicitly override the CQN properties instead of using the Fluent API when you don't want to append to existing clauses. For example:
const { columns, orders } = cds.ql
let q1 = SELECT`a,b`.from`Foo`.where`x>1`.orderBy`a`
let q2 = cds.ql.clone(q1)
q2.SELECT.columns = columns`c,d,e`
q2.SELECT.orderBy = orders`b`Kill Switch
As a last resort, restore the former behavior with the following config option:
cds.features.... | -> restores: |
|---|---|
| compat_clone_appends: true | the erroneous former behavior |
Warning
As such kill switches restore unintended, and erroneous behavior, they should only be used as a temporary measure. It's recommended to update your code to be compatible with the new behavior. The kill switches will be removed in a future release, and relying on them for longer time may cause maintenance issues and technical debt.
Fixed Bulk Inserts via REST
CAP services generally support bulk inserts of multiple entries like so:
await srv.create(Books).entries(
{ title: 'Book 1', stock: 10 },
{ title: 'Book 2', stock: 20 },
{ title: 'Book 3', stock: 30 }
)And the same can be done via REST as well, for example, like that:
POST /Books
Content-Type: application/json
[
{ "title": "Book 1", "stock": 10 },
{ "title": "Book 2", "stock": 20 },
{ "title": "Book 3", "stock": 30 }
]However, the REST adapter did not support this properly before. It silently converted bulk creates into multiple single creates, so custom handlers for CREATE requests did not receive the complete set of entries and had no chance to optimize processing, for example, by delegating the bulk insert to the database service. This has been fixed with cds10.
Are you affected?
You are only affected by this change if all of the below conditions are true:
- You have a service exposed via REST
- Your clients send bulk create requests to these services
- You handle these requests with custom handlers, and ...
- you expect the former behavior of
req.databeing a single object, or - you expect only one entry in
req.query.INSERT.entrieswithin these handlers.
How to address?
Make the custom handlers aware of bulk inserts, for example, by delegating the bulk insert to the database service:
this.on ('CREATE', Books, req => INSERT.into(Books).entries(req.data))Kill Switch
As a last resort, restore the former behavior with the following config option:
cds.features.... | -> restores: |
|---|---|
| bulk_inserts_via_rest: false | the erroneous former behavior |
Warning
As such kill switches restore unintended, and erroneous behavior, they should only be used as a temporary measure. It's recommended to update your code to be compatible with the new behavior. The kill switches will be removed in a future release, and relying on them for longer time may cause maintenance issues and technical debt.
Non-Breaking Changes
The following changes are mostly the result of refactoring or re-implementing certain CAP features to stay current with Node.js and to reduce dependencies on unmaintained third-party packages.
Non-breaking
They are non-breaking in the sense that they do not affect any APIs, and you should not notice any change in behavior at all. Still find below instructions on how to address potential issues, in case you encounter any.
Node-native Fetch API
Already in April 26 we replaced all direct usages of Axios by using Node's native implementation of the standard Fetch API.
As part of this change, we now also use the native Fetch API for remote service consumption during development by default, and require SAP Cloud SDK only in production.
If you prefer to continue using SAP Cloud SDK during development as well, install the @sap-cloud-sdk/http-client package:
npm add @sap-cloud-sdk/http-clientNode-native SQLite
In Feb 26, we introduced a new Node-native SQLite implementation. It reached GA and became the default in cds10, so better-sqlite3 is no longer installed through @cap-js/sqlite.
This change is not breaking, and you should not notice any difference. If needed, you can switch back to the former implementation as follows:
- Add
better-sqlite3as a dev dependency to your project:
npm add -D better-sqlite3- Use the cds.requires.db.driver option to configure the project to use that driver:
"cds": {
"requires": {
"db": {
"driver": "better-sqlite3"
}
}
}New Connection Pool
With cds10 we replaced the former connection pool implementation based on the 3rd-party package generic-pool by a new CAP-native implementation, which is fully compatible with the former.
This change is not breaking, and you should not notice any difference. If needed, you can switch back to the former implementation as follows:
- Add
generic-poolas a dev dependency to your project:
npm add generic-pool- Use the cds.features.pool option to configure the project to use that driver:
"cds": {
"features": {
"pool": "generic-pool"
}
}Fixed Affinity for Decimals
To avoid unexpected integer division effects with Decimal elements in SQLite, we changed the type affinity from NUMERIC to REAL by changing the generated column type from DECIMAL to REAL_DECIMAL for SQLite:
entity E { d: Decimal }CREATE table E ( d DECIMAL ); -- NUMERIC affinity
CREATE table E ( d REAL_DECIMAL ); -- REAL affinityWe can demonstrate the effect of the former NUMERIC affinity with the following SQL snippet (you can run that in sqlite3 CLI):
CREATE table T ( a REAL_DECIMAL, b DECIMAL );
INSERT into T values ( 2.0, 2.0 );
SELECT 1/a from T;
SELECT 1/b from T;0.5 -- correct result with REAL affinity
0 -- unexpected integer division due to NUMERIC affinityNon-breaking, and only for SQLite
This change is not breaking, and affects only SQLite. No changes apply to SAP HANA or PostgreSQL. Still, if you want to restore the former behavior, you can do so by setting cds.requires.db.decimal_affinity: 'numeric'.
Flags Entirely Removed
The following flags already had the new fixed behavior as their default in cds 9, but you could still revert to the former erroneous behavior. In cds 10, these flags are removed entirely and are ignored if set in your project.
| Removed Flag | Fixed behavior | Default | Since |
|---|---|---|---|
| consistent_params | Confusing req.params -> always array now | true | May 25 |
| compat_save_drafts | Draft SAVE handlers called on PATCH events | false | Sep 25 |
| compat_assert_not_null | ASSERT_MANDATORY instead of _NOT_NULL | false | Sep 25 |
If you still use any of these in your project you must fix your code now!
Follow the instructions in the linked release notes sections to fix your code.
Change Tracking Plugin v2
The @cap-js/change-tracking plugin has been upgraded to major version 2.0, which introduces significant improvements:
- Database triggers for change tracking, resulting in major performance improvements
- Hierarchical view of changes across parent and child entities
- And more...
Are you affected?
- yes, if you use any 1.x version of
@cap-js/change-tracking
How to address?
Upgrade to the new version by following the migration guide, which also includes an SAP HANA migration table to ensure existing change log data is retained during the upgrade.