DJ Adams

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:

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 using await on a query object will by default invoke db.run(), sending that query object as an argument (i.e. db.run(insert) here). However, this db service is the "wrong" one - it's the persistence-layer database service, rather than the AdminService at 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

Comparing the artifacts and their CSN shows differences that all make sense (each of these comparisons shows A first, then B):

And perhaps most significantly:

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!