CDS expressions in CAP - notes on Part 1
Notes to accompany Part 1 of the mini-series on the core expression language in CDS.
See the series post for an overview of all the episodes.
Notes
00:00 Introductions.
05:00 A first look at expressions in the context of the new Declarative Constraints feature.
06:00 An overview of CDS and the languages within the CDS family (CDL & CSN, CQL & CQN and CXL & CXN).
07:05 A first look at the
"cxl-bookshop" project, specifically the entities and service definition.
Patrice compiled (with cds compile db/schema.cds) the schema level
definitions to show the relationship between the human-first
CDL (Conceptual Definition Language) and
the corresponding machine-first CSN (Core
Schema Notation).
11:38 Next in the CDS
language family overview is the human-first
CQL (CDS Query Language) and
corresponding machine-first equivalent
CQN (CDS Query Notation). An example was
given in the context of a cds REPL session (started with cds repl --run .).
The example uses a postfix projection to specify the desired column (element),
instead of the more SQL-like SELECT title from Books (the resulting query is
the same):
> q = cds.ql` SELECT from Books { title }`
cds.ql {
SELECT: {
from: { ref: [ 'Books' ] },
columns: [ { ref: [ 'title' ] } ]
}
}
This shows the CQL and the corresponding CQN.
19:58 The final language pair, the human-first CXL (CDS Expression Language) and the corresponding machine-first CXN (CDS Expression Notation) (at this point the CXL documentation in Capire was still under construction, but exists now).
27:23 Looking at very
simple expressions, trying out the (context-free) expression parser
cds.parse.expr to parse a CXL expression to the internal CXN
representation:
> cds.parse.expr` 1 `
{ val: 1 }
Here's one with a binary operator (+), taking two operands:
> cds.parse.expr` 1 + 1 `
{ xpr: [ { val: 1 }, '+', { val: 1 } ] }
29:01 An example in the
context of declarative constraints, where validation is added to a service
entity (which makes sense as they act as functional facets to the underlying
persistence layer), in particular, to the stock element of the Books
entity:
using {sap.capire.bookshop as my} from '../db/schema';
service AdminService {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
annotate AdminService.Books:stock with @assert: (
case
when stock < 0 then 'stock must not be negative'
when stock > 1000 then 'stock exceeds maximum limit of 1000'
end
);
Note that here we have:
- the
annotatedirective (CDL) - the
@assertannotation (also CDL) - the
case ... when ... then ... endexpression (CXL)
There was a brief discussion on how expressions in annotations should be enclosed in parentheses (as used in this annotation example), which also allows full compiler support and also code completion.
34:58 Exercising the constraint, from within the cds REPL. First, an overview of the entities:
> Object.keys(AdminService.entities)
[
'Books',
'Authors',
'Genres',
'Genres.texts',
'Currencies',
'Currencies.texts',
'Books.texts'
]
and then some destructuring to get the Books entity from the AdminService:
{ Books } = AdminService.entities
[object Function]
Next, the construction of an INSERT query via the fluent
API (the statement is split
across multiple lines for readability; use the REPL's .editor command to
paste in these multiple lines if you're playing along):
> insert = INSERT
.into(Books)
.entries(
{ ID:567, author_ID: 180, title: 'Foo', stock: -1 },
{ ID: 568, author_ID: 180, title: 'Foo 2', stock: 1001 }
)
which gives:
cds.ql {
INSERT: {
into: { ref: [ 'AdminService.Books' ] },
entries: [
{ ID: 567, author_ID: 180, title: 'Foo', stock: -1 },
{ ID: 568, author_ID: 180, title: 'Foo 2', stock: 1001 }
]
}
}
At this point, this query was
awaited, with an unexpected result. That's because usingawaiton a query object will by default invokedb.run(), sending that query object as an argument (i.e.db.run(insert)here). However, thisdbservice is the "wrong" one - it's the persistence-layer database service, rather than theAdminServiceat which level we have our declarative constraint. This is why the insertion of the two new book records was successful (at 39:50).
Sending this to the AdminService to be executed:
> AdminService.run(insert)
shows us the result of the constraint:
> Uncaught [Error: MULTIPLE_ERRORS] {
details: [
{
status: 400,
code: 'ASSERT',
target: 'stock',
numericSeverity: 4,
'@Common.numericSeverity': 4,
message: 'stock must not be negative'
},
{
status: 400,
code: 'ASSERT',
target: 'stock',
numericSeverity: 4,
'@Common.numericSeverity': 4,
message: 'stock exceeds maximum limit of 1000'
}
]
}
46:25 A brief comparison
between the Books definition at the persistence ("db") level from
cds.entities and the Books definition in the AdminService at the service
("srv") level. This is effectively the difference between A and B where:
using {sap.capire.bookshop as my} from '../db/schema';
service AdminService {
entity Books as projection on my.Books;
^ ^
| |
B A
- A
my.Booksis fromsap.capire.bookshopindb/schema - B
Booksis the entity defined inAdminServiceas a projection onmy.Booksin A
Comparing the artifacts and their CSN shows differences that all make sense (each of these comparisons shows A first, then B):
-
the artifacts'
nameproperties (Books.name) are different:sap.capire.bookshop.BooksvsAdminService.Books -
reflecting this, the scope name prefixes are different in each reference, with A being
sap.capire.bookshopand B beingAdminService, for example:author: Association { ... target: 'sap.capire.bookshop.Authors' }vs
author: Association { ... target: 'AdminService.Authors' } -
each association in the service level CSN has an additional annotation
@Common.ValueList.viaAssociation, for example (although we didn't dwell on this at all):genre: Association { type: 'cds.Association', target: 'AdminService.Genres', keys: [ { ref: [ 'ID' ], '$generatedFieldName': 'genre_ID' } ], '@Common.ValueList.viaAssociation': { '=': 'genre' } } -
as pointed out in the conversation, the service level CSN has an extra
projectionproperty, which makes sense given how this is defined:projection: { from: { ref: [ 'sap.capire.bookshop.Books' ] } }
And perhaps most significantly:
-
while the
stockelement at the persistence level is defined like this:stock: Integer { type: 'cds.Integer' }the projected version, thanks to the annotation applied specifically to
AdminService.Books:stock, is defined like this (where can see the expression in CXN form):stock: Integer { '@assert': { '=': "case when stock < 0 then 'stock must not be negative' when stock > 1000 then 'stock exceeds maximum limit of 1000' end", xpr: [ 'case', 'when', { ref: [ 'stock' ] }, '<', { val: 0 }, 'then', { val: 'stock must not be negative' }, 'when', { ref: [ 'stock' ] }, '>', { val: 1000 }, 'then', { val: 'stock exceeds maximum limit of 1000' }, 'end' ] }, type: 'cds.Integer' }
57:44 Patrice describes the relationship between CXL, CQL and CDL in terms of "passenger" and "vehicle", in that CXL expressions are "passengers" carried in the "vehicles" of CQL queries and / or CDL definitions (such as annotations and element definitions); Patrice illustrates this further by showing what a simple expression in CXL is parsed to:
> cds.parse.expr` author.books.genre.name `
{ ref: [ 'author', 'books', 'genre', 'name' ] }
and also notes that the value of the ref is interpreted depending on the
"vehicle" in which it is being carried.
Further info
Patrice also has some great notes for this part, definitely worth perusing too!
- ← Previous
Genres, cuids and a bit of AWK