Upgrading to @sap/cds v10
This guide lists all configuration defaults that change in cds 10, their impact, and how to identify affected code. There are three categories:
- Opt-ins becoming defaults — existing opt-in flags will flip to their new value. You can already switch to the cds 10 default behavior today to reduce migration effort later.
- Deprecated opt-outs being removed — temporary compat flags are deleted entirely. If you deferred adjustments so far, you must make them now.
- Removed features/APIs — previously deprecated features and APIs that are removed entirely in cds 10.
Potentially Breaking Changes
Fixed cds.clone(q).orderBy(...) behavior
Default Value Changes
These defaults flip in cds 10. Most projects will need to verify compatibility.
| Config | cds 9 | cds 10 | Impact |
|---|---|---|---|
ieee754compatible | false | true | Decimal/Int64 returned as strings on SQLite and PostgreSQL (as already done on HANA) |
count_as_string | false | true | $count returned as string (per OData Edm.Int64) |
move_media_data_in_db | false | true | cds.LargeBinary copied from draft to active table in DB -> needs discussion |
compat_texts_entities | true | false | Use definitions.Books.texts instead of definitions['Books.texts'] |
legacyLocking | true | false | Queue switches to status-based locking |
To opt in early, set the cds 10 value in your project configuration now:
{ "cds": { "features": {
"ieee754compatible": true,
"count_as_string": true,
"compat_texts_entities": false
}, "fiori": {
"move_media_data_in_db": true
}, "requires": { "queue": {
"legacyLocking": false
}}}}ieee754compatible
| Key | cds.features.ieee754compatible |
| cds 9 default | false |
| cds 10 default | true |
Impact:Decimal and Int64 values from SQLite and PostgreSQL are now returned as strings instead of numbers. SAP HANA already returned strings, so this aligns behavior across databases and profiles (i.e., development/test where SQLite is the default vs. production where HANA is the default).
// Given: entity Foo { dec: Decimal; i64: Int64; }
let foos = await SELECT.from('Foo')
// cds 9 (ieee754compatible = false):
// [{ dec: 123.45, i64: 12345 }] ← numbers on SQLite/PostgreSQL
// cds 10 (ieee754compatible = true):
// [{ dec: '123.45', i64: '12345' }] ← strings, aligned with HANAWhat to adjust: Tests comparing numeric results with strict equality (e.g. expect(row.price).to.equal(9.99)) must compare against strings ("9.99") or use loose equality. OData responses change from "Price": 9.99 to "Price": "9.99" (aligned with the respective EDM types). Input can still be plain numbers, bigints, or strings.
How to identify:
npx -y -p @ast-grep/cli ast-grep -p 'expect($X).to.equal($Y)' -l js test/
npx -y -p @ast-grep/cli ast-grep -p 'assert.equal($X, $Y)' -l js test/
npx -y -p @ast-grep/cli ast-grep -p 'assert.strictEqual($X, $Y)' -l js test/Look for numeric comparisons on Decimal / Int64 fields. Also check OData response snapshot tests.
count_as_string
| Key | cds.features.count_as_string |
| cds 9 default | false |
| cds 10 default | true |
Impact:$count results in OData responses are returned as strings instead of numbers. This is aligned with the OData specification, which defines counts as Edm.Int64.
What to adjust: Tests asserting res.data["@odata.count"] as a number must switch to string comparison, e.g. expect(count).to.equal("42") instead of expect(count).to.equal(42).
How to identify:
npx -y -p @ast-grep/cli ast-grep -p '$X["@odata.count"]' -l js test/
npx -y -p @ast-grep/cli ast-grep -p '$X.$count' -l js test/Look for numeric assertions on count values.
move_media_data_in_db
| Key | cds.fiori.move_media_data_in_db |
| cds 9 default | false |
| cds 10 default | true |
Impact: Properties of type cds.LargeBinary are now copied from the draft table to the active table in the database during draft activation, rather than via application-level streaming.
What to adjust: Generally no code change needed unless you have custom SAVE/UPDATE handlers that manipulate binary data during activation. Test drafts with media attachments to ensure activation still works. Known issue: SAP HANA hdb driver may deadlock — use hana-client driver instead.
How to identify:
grep -rn 'LargeBinary\|MediaData\|@Core.MediaType' db/ srv/Find entities with binary/media properties that are draft-enabled.
compat_texts_entities
| Key | cds.features.compat_texts_entities |
| cds 9 default | true |
| cds 10 default | false |
Impact: Localized text entities are no longer accessible via bracket notation definitions['Books.texts']. Use property access definitions.Books.texts instead. Text entities are also no longer included in srv.entities results.
What to adjust: Replace cds.model.definitions['<Entity>.texts'] with cds.model.definitions.<Entity>.texts.
How to identify:
npx -y -p @ast-grep/cli ast-grep -p '$X.definitions[$Y]' -l js srv/ test/legacyLocking
| Key | cds.requires.queue.legacyLocking |
| cds 9 default | true |
| cds 10 default | false |
Impact: Persistent queue switches from long-lived DB locks to status-based locking. Reduces lock duration but changes reprocessing semantics — abandoned messages are retried after a configurable timeout (default "1h").
What to adjust: No adjustment necessary if you are upgrading from cds 9 to cds 10. Status-based locking was introduced in cds 9 but could not be made the default right away due to backward compatibility concerns. If you are upgrading from cds <= 8 (not recommended), consider upgrading with downtime such that no app instances running cds <= 8 try to process an event added to the queue by cds 10.
How to identify:
grep -rn 'legacyLocking\|maxAttempts\|outbox\|persistent-outbox' package.json .cdsrc.json srv/Check for explicit queue config.
Compat Opt-Outs Being Removed
These flags already had the new behavior as default in cds 9. In cds 10 the old compat flag is removed entirely — if your project explicitly sets the legacy value, it will be ignored and the new behavior is enforced.
If you have any of these in your project (i.e., a past change was somehow breaking for you and you deferred adjustments), you must adjust now.
The following temporary opt-outs will be deleted. If your project still uses any of these, you must adjust now:
| Removed opt-out | Enforced behavior |
|---|---|
cds.features.service_level_restrictions = false | @requires always enforced on local service calls |
cds.features.compat_save_drafts = true | SAVE handlers always triggered on draft activation |
cds.features.async_handler_compat | → hidden guide? |
cds.fiori.calc_elements = false | Calculated elements always computed for drafts → that was quite breaking for some? |
cds.features.consistent_params = false | req.params always array of objects |
cds.features.locale_fallback = true | No automatic locale fallback |
cds.features.compat_assert_not_null = true | Error code always ASSERT_MANDATORY |
cds.features.odata_metadata_compat = true | EDMX recompiled only when necessary |
service_level_restrictions
| Key | cds.features.service_level_restrictions |
| Legacy value | false |
| Enforced behavior | Service-level @requires is always enforced, even for local in-app calls |
What to adjust: Internal service calls must use a privileged user or carry proper authorization. Tests calling services without auth context will get 403.
// Use cds.User.Privileged for internal/technical calls:
this.before('*', function (req) {
const user = new cds.User.Privileged
return this.tx({ user }, tx => tx.run(
INSERT.into('AuditLog').entries({ url: req._.req.url })
))
})
// Or use the ready-made instance:
const privileged = cds.User.privilegedHow to identify:
grep -rn 'service_level_restrictions' .cdsrc.json package.json
npx -y -p @ast-grep/cli ast-grep -p 'srv.send($$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p 'srv.run($$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p 'srv.emit($$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p 'srv.dispatch($$$)' -l js srv/async_handler_compat
→ missing in list
compat_save_drafts
| Key | cds.features.compat_save_drafts |
| Legacy value | true |
| Enforced behavior | SAVE handlers are always triggered on draft activation |
What to adjust: If you have srv.on('SAVE', ...) or srv.before('SAVE', ...) handlers, they now fire on draft activation too. Ensure these handlers are idempotent for both direct save and draft-activate paths. Note that SAVE handlers are registered on MyEntity.drafts, not MyEntity:
srv.before('SAVE', MyEntity.drafts, async (req) => {
// This now fires on EVERY activation — make sure it's idempotent
})How to identify:
grep -rn 'compat_save_drafts' .cdsrc.json package.json
npx -y -p @ast-grep/cli ast-grep -p '$X.on("SAVE", $$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p '$X.before("SAVE", $$$)' -l js srv/calc_elements
| Key | cds.fiori.calc_elements |
| Legacy value | false |
| Enforced behavior | Calculated elements are always computed for draft reads |
What to adjust: If tests assert on draft data that previously returned null or undefined for calculated fields, they now return computed values.
How to identify:
grep -rn 'calc_elements' .cdsrc.json package.jsonLook for element ... = ...; in CDS models with draft-enabled entities.
consistent_params
| Key | cds.features.consistent_params |
| Legacy value | false |
| Enforced behavior | req.params is always an array of objects (e.g. [{ID: 101}]) |
What to adjust:req.params now always returns key-value objects, even for single-keyed entities where it previously returned the scalar value directly:
// GET /catalog/Authors(101)/books(title='Eleonora',edition=2)
const [author, book] = req.params
// cds 10: author === { ID: 101 } ← was just 101 before
// cds 10: book === { title: 'Eleonora', edition: 2 }Replace req.params[0] (expecting a scalar) with req.params[0].ID or destructure: const [{ID}] = req.params.
How to identify:
grep -rn 'consistent_params' .cdsrc.json package.json
npx -y -p @ast-grep/cli ast-grep -p '$X.params[$X]' -l js srv/ test/locale_fallback
| Key | cds.features.locale_fallback |
| Legacy value | true |
| Enforced behavior | No automatic locale fallback — cds.context.locale is undefined if none provided |
What to adjust: Tests relying on localized data being returned for unauthenticated/locale-less requests may see different sort orders or untranslated data. Set Accept-Language header explicitly.
How to identify:
grep -rn 'locale_fallback' .cdsrc.json package.jsonLook for tests making requests without Accept-Language that assert on localized text.
compat_assert_not_null
| Key | cds.features.compat_assert_not_null |
| Legacy value | true |
| Enforced behavior | Error code is always ASSERT_MANDATORY (not ASSERT_NOT_NULL) |
What to adjust: Update tests checking err.code === 'ASSERT_NOT_NULL' to 'ASSERT_MANDATORY'. Update custom i18n translations keyed on the old error code.
How to identify:
grep -rn 'ASSERT_NOT_NULL\|compat_assert_not_null' srv/ test/ .cdsrc.json _i18n/odata_metadata_compat
| Key | cds.features.odata_metadata_compat |
| Legacy value | true |
| Enforced behavior | Model recompilation for EDMX only when necessary (no forced recompilation in multitenancy) |
What to adjust: Generally transparent. If you had custom EDMX post-processing relying on always-fresh compilation, verify it still works. Only relevant for multitenant apps.
How to identify:
grep -rn 'odata_metadata_compat' .cdsrc.json package.jsonRemoved Features/APIs
srv.entities() and similar function forms
The undocumented function forms srv.entities(), srv.types(), srv.events(), and srv.actions() are removed — including this.entities() etc. when used inside service implementations. Use the property forms instead:
| Removed | Replacement |
|---|---|
srv.entities() / this.entities() | srv.entities / cds.entities() |
srv.types() / this.types() | srv.types |
srv.events() / this.events() | srv.events |
srv.actions() / this.actions() | srv.actions |
How to identify:
npx -y -p @ast-grep/cli ast-grep -p '$X.entities()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.types()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.events()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.actions()' -l js srv/ test/Other preparations
- Node.js 22 will be the minimum supported runtime. Node.js 24 (LTS) is recommended, followed by Node.js 26 in October 2026.
- Native
node:sqlitebecomes the default SQLite driver. Alternative driversbetter-sqlite3andsql.jsrequire an explicit dependency.
Quick Scan Script
Run this to check whether your project is affected by any of the above:
# Check for any cds 10 compat flags in project config
grep -rn 'ieee754compatible\|count_as_string\|move_media_data_in_db\|compat_texts_entities\|service_level_restrictions\|compat_save_drafts\|calc_elements\|consistent_params\|locale_fallback\|compat_assert_not_null\|odata_metadata_compat\|legacyLocking' \
package.json .cdsrc.json .cdsrc-private.json 2>/dev/null
# Check for ASSERT_NOT_NULL references
grep -rn 'ASSERT_NOT_NULL' srv/ test/ _i18n/ 2>/dev/null
# Check for bracket-access to .texts entities
npx -y -p @ast-grep/cli ast-grep -p '$X.definitions[$Y]' -l js srv/ test/
# Check for deprecated srv.entities() / this.entities() function calls
npx -y -p @ast-grep/cli ast-grep -p '$X.entities()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.types()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.events()' -l js srv/ test/
npx -y -p @ast-grep/cli ast-grep -p '$X.actions()' -l js srv/ test/
# Check for numeric assertions on Decimal/count fields in tests
npx -y -p @ast-grep/cli ast-grep -p 'expect($X).to.equal($Y)' -l js test/
# Check for numeric assertions on $count
npx -y -p @ast-grep/cli ast-grep -p '$X["@odata.count"]' -l js test/
# Check for LargeBinary / media properties in draft-enabled entities
grep -rn 'LargeBinary\|MediaData\|@Core.MediaType' db/ srv/ 2>/dev/null
# Check for internal service calls that may need privileged user
npx -y -p @ast-grep/cli ast-grep -p '$X.send($$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p '$X.run($$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p '$X.emit($$$)' -l js srv/
# Check for SAVE handlers affected by draft activation
npx -y -p @ast-grep/cli ast-grep -p '$X.on("SAVE", $$$)' -l js srv/
npx -y -p @ast-grep/cli ast-grep -p '$X.before("SAVE", $$$)' -l js srv/
# Check for req.params scalar access patterns
npx -y -p @ast-grep/cli ast-grep -p '$X.params[$X]' -l js srv/ test/