From 3361910bd9e442db15708a64a064d79b156779da Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 31 Jul 2026 15:09:22 +0200 Subject: [PATCH 1/4] CQL w/ live snippets --- .../db/data/sap.capire.bookshop-Addresses.csv | 5 ++ .../db/data/sap.capire.bookshop-Authors.csv | 11 +-- .../db/data/sap.capire.bookshop-Books.csv | 1 + .../db/data/sap.capire.bookshop-Towns.csv | 5 ++ .../templates/bookshop/db/schema.cds | 17 ++++ cds/cql.md | 79 +++++++++++-------- 6 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 .vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv create mode 100644 .vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv new file mode 100644 index 0000000000..bac27500cf --- /dev/null +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv @@ -0,0 +1,5 @@ +ID,street,town_ID +1,6 Place des Vosges,1 +2,Church Street,2 +3,North Street,3 +4,King Street,4 \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv index 9b418c17f2..d0f9f0c48c 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv @@ -1,5 +1,6 @@ -ID,name,dateOfBirth,placeOfBirth,dateOfDeath,placeOfDeath -101,Emily Brontë,1818-07-30,"Thornton, Yorkshire",1848-12-19,"Haworth, Yorkshire" -107,Charlotte Brontë,1818-04-21,"Thornton, Yorkshire",1855-03-31,"Haworth, Yorkshire" -150,Edgar Allen Poe,1809-01-19,"Boston, Massachusetts",1849-10-07,"Baltimore, Maryland" -170,Richard Carpenter,1929-08-14,"King’s Lynn, Norfolk",2012-02-26,"Hertfordshire, England" +ID,name,dateOfBirth,placeOfBirth,dateOfDeath,placeOfDeath,address_ID +10,Victor Hugo,1802-02-26,"Besançon, Franche-Comté",1885-05-22,"Paris, Île-de-France",1 +101,Emily Brontë,1818-07-30,"Thornton, Yorkshire",1848-12-19,"Haworth, Yorkshire",2 +107,Charlotte Brontë,1818-04-21,"Thornton, Yorkshire",1855-03-31,"Haworth, Yorkshire",2 +150,Edgar Allen Poe,1809-01-19,"Boston, Massachusetts",1849-10-07,"Baltimore, Maryland",3 +170,Richard Carpenter,1929-08-14,"King’s Lynn, Norfolk",2012-02-26,"Hertfordshire, England",4 diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv index d9cc9ee2ee..87ff63081e 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv @@ -4,3 +4,4 @@ ID,title,descr,author_ID,stock,price,currency_code,genre_ID 251,The Raven,"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.",150,333,13.13,USD,16aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa 252,Eleonora,"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.",150,555,14,USD,15aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa 271,Catweazle,"Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.",170,22,150,JPY,13aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +281,Les Misérables,"Les Misérables (French pronunciation: ​[le mizeʁabl]) is a French historical novel by Victor Hugo, first published in 1862, that is considered one of the greatest novels of the 19th century. In the English-speaking world, the novel is usually referred to by its original French title, although it is sometimes translated as The Miserable Ones, The Wretched, or The Poor Ones. The story examines the nature of law and grace, and expounds upon the history of France, the architecture and urban design of Paris, politics, moral philosophy, antimonarchism, justice, religion, and the types and nature of romantic and familial love.",10,33,20.20,EUR,12aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv new file mode 100644 index 0000000000..882b2840a3 --- /dev/null +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv @@ -0,0 +1,5 @@ +ID,name,zip,country +1,Paris,75000,France +2,Thornton,NN14,UK +3,Boston,02108,USA +4,King’s Lynn,PE30,UK \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds b/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds index 8c510a3599..763744a288 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds @@ -27,6 +27,23 @@ entity Authors { age = years_between(dateOfBirth, coalesce(dateOfDeath, date( $now ))); } +extend Authors with { + address : Association to Addresses; +} + +entity Addresses { + key ID : Integer; + street : String; + town : Association to Towns; +} + +entity Towns { + key ID : Integer; + name : String; + zip : String; + country : String; +} + /** Hierarchically organized Code List for Genres */ entity Genres : cuid, sap.common.CodeList { parent : Association to Genres; diff --git a/cds/cql.md b/cds/cql.md index bf8d5154fd..563eef36f8 100644 --- a/cds/cql.md +++ b/cds/cql.md @@ -17,10 +17,10 @@ CDS Query Language (CQL) is based on standard SQL, which it enhances by... CQL allows to put projections, that means, the `SELECT` clause, behind the `FROM` clause enclosed in curly braces. For example, the following are equivalent: -```sql +```cds live SELECT name, address.street from Authors ``` -```sql +```cds live SELECT from Authors { name, address.street } ``` @@ -30,7 +30,7 @@ SELECT from Authors { name, address.street } Postfix projections can be appended to any column referring to a struct element or an association and hence be nested. This allows **expand** results along associations and hence read deeply structured documents: -```sql +```cds live SELECT from Authors { name, address { street, town { name, country }} }; @@ -67,7 +67,7 @@ Nested Expands following _to-many_ associations are not supported. As the name of the struct element or association preceding the postfix projection appears in the result set, an alias can be provided for it: -```sql +```cds live SELECT from Authors { name, address as residence { street, town as city { name, country }} }; @@ -95,7 +95,7 @@ Nested Expands can contain expressions. In addition, it's possible to define new structures that aren't present in the data source. In this case an alias is mandatory and is placed *behind* the `{…}`: -```sql +```cds live SELECT from Books { title, author { name, dateOfDeath - dateOfBirth as age }, @@ -125,7 +125,7 @@ results = [ Put a **`"."`** before the opening brace to **inline** the target elements and avoid writing lengthy lists of paths to read several elements from the same target. For example: -```sql +```cds live SELECT from Authors { name, address.{ street, town.{ name, country }} }; @@ -133,7 +133,7 @@ SELECT from Authors { … is equivalent to: -```sql +```cds live SELECT from Authors { name, address.street, @@ -144,7 +144,7 @@ SELECT from Authors { Nested Inlines can contain expressions: -```sql +```cds live SELECT from Books { title, author.{ @@ -156,7 +156,7 @@ SELECT from Books { The previous example is equivalent to the following: -```sql +```cds live SELECT from Books { title, author.name, @@ -172,7 +172,7 @@ Within postfix projections, the `*` operator queries are handled slightly differ #### Example: -```sql +```cds live SELECT from Books { *, author.name as author } ``` @@ -183,7 +183,7 @@ Queries like in our example, would result in duplicate element effects for `auth Use the `excluding` clause in combination with `SELECT *` to select all elements except for the ones listed in the exclude list. -```sql +```cds live SELECT from Books { * } excluding { author } ``` @@ -222,11 +222,11 @@ SELECT * from Boo --> { foo, car } If the `*` selector is used following an association, it selects all elements of the association target. For example, the following queries are equivalent: -```sql +```cds live SELECT from Books { title, author { * } } ``` -```sql -SELECT from Books { title, author { ID, name, dateOfBirth, … } } +```cds live +SELECT from Books { title, author { ID, name, dateOfBirth, /*…*/ } } ``` @@ -239,7 +239,7 @@ SELECT from Authors { name, struc } ``` The `excluding` clause can also be used for Nested Expands: -```sql +```cds live SELECT from Books { title, author { * } excluding { dateOfDeath, placeOfDeath } } ``` @@ -249,13 +249,15 @@ SELECT from Books { title, author { * } excluding { dateOfDeath, placeOfDeath } The expansion of `*` in Nested Inlines is analogous. The following queries are equivalent: -```sql +```cds live SELECT from Books { title, author.{ * } } -SELECT from Books { title, author.{ ID, name, dateOfBirth, … } } +``` +```cds live +SELECT from Books { title, author.{ ID, name, dateOfBirth, /*…*/ } } ``` The `excluding` clause can also be used for Nested Inlines: -```sql +```cds live SELECT from Books { title, author.{ * } excluding { dateOfDeath, placeOfDeath } } ``` @@ -265,19 +267,23 @@ SELECT from Books { title, author.{ * } excluding { dateOfDeath, placeOfDeath } Use path expressions to navigate along associations and/or struct elements in any of the SQL clauses as follows: In `from` clauses: -```sql +```cds live SELECT from Authors[name='Emily Brontë'].books; -SELECT from Books:authors.towns; +``` +```cds live +SELECT from Books:author.address.town; ``` In `select` clauses: -```sql +```cds live SELECT title, author.name from Books; +``` +```cds live SELECT *, author.address.town.name from Books; ``` In `where` clauses: -```sql +```cds live SELECT from Books where author.name='Emily Brontë' ``` @@ -289,12 +295,14 @@ The same is valid for `group by`, `having`, and `order by`. Path expressions in from clauses allow to fetch only those entries from a target entity, which are associated to a parent entity. They unfold to _SEMI JOINS_ in plain SQL queries. For example, the previous mentioned queries would unfold to the following plain SQL counterparts: ```sql +-- plain SQL SELECT * from Books WHERE EXISTS ( SELECT 1 from Authors WHERE Authors.ID = Books.author_ID AND Authors.name='Emily Brontë' ); ``` ```sql +-- plain SQL SELECT * from Towns WHERE EXISTS ( SELECT 1 from Authors WHERE Authors.town_ID = Towns.ID AND EXISTS ( SELECT 1 from Books WHERE Books.author_ID = Authors.ID @@ -333,7 +341,7 @@ All column references get qualified → in contrast to plain SQL joins there' Append infix filters to associations in path expressions to narrow the resulting joins. For example: -```sql +```cds live SELECT books[genre='Mystery'].title from Authors WHERE name='Agatha Christie' ``` @@ -348,7 +356,7 @@ WHERE Authors.name='Agatha Christie'; If an infix filter effectively reduces the cardinality of a *to-many* association to *one*, make this explicit with: -```sql +```cds SELECT name, books[1: favorite=true].title from Authors ``` @@ -361,28 +369,29 @@ that the specified condition reduces the result to a single entry. Use a filtered path expression to test if any element of the associated collection matches the given filter: -```sql -SELECT FROM Authors {name} WHERE EXISTS books[year = 2000] +```cds live +SELECT FROM Authors {name} WHERE EXISTS books[stock = 11] ``` ...unfolds to: ```sql +-- plain SQL SELECT name FROM Authors WHERE EXISTS ( SELECT 1 FROM Books WHERE Books.author_id = Authors.id - AND Books.year = 2000 + AND Books.stock = 11 ) ``` Exists predicates can be nested: -```sql +```cds SELECT FROM Authors { name } WHERE EXISTS books[year = 2000 and EXISTS pages[wordcount > 1000]] ``` A path with several associations is rewritten as nested exists predicates. The previous query is equivalent to the following query. -```sql +```cds SELECT FROM Authors { name } WHERE EXISTS books[year = 2000].pages[wordcount > 1000] ``` @@ -397,13 +406,17 @@ Paths *inside* the filter are not yet supported. There are two different constructs commonly called casts. SQL casts and CDL casts. The former produces SQL casts when rendered into SQL, whereas the latter does not: -```sql -SELECT cast (foo+1 as Decimal) as bar from Foo; -- standard SQL -SELECT from Foo { foo+1 as bar : Decimal }; -- CDL-style +```cds live +// standard SQL +SELECT price, cast (price+1 as Integer) as pr from Books; +``` +```cds live +// CDL-style +SELECT from Books { price, price+1 as pr : Integer }; ``` [Learn more about CDL type definitions](./cdl#types){.learn-more} -Use SQL casts when you actually want a cast in SQL. CDL casts are useful for expressions such as `foo+1` as the compiler does not deduce types. +Use SQL casts when you actually want a cast in SQL. CDL casts are useful for expressions such as `price+1` as the compiler does not deduce types. For the OData backend, by specifying a type, the compiler will also assign the correct EDM type in the generated EDM(X) files. ::: tip From 1b2522b98e7f94d3530ee7ec3bd7fda874d5ba03 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Tue, 4 Aug 2026 10:33:26 +0200 Subject: [PATCH 2/4] Multimodel support in live queries --- cds/cql.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cds/cql.md b/cds/cql.md index 563eef36f8..1467f82041 100644 --- a/cds/cql.md +++ b/cds/cql.md @@ -190,7 +190,7 @@ SELECT from Books { * } excluding { author } The effect is about **late materialization** of signatures and staying open to late extensions. For example, assume the following definitions: -```cds +```cds [FooBar] entity Foo { foo : String; bar : String; car : String; } entity Bar as select from Foo excluding { bar }; entity Boo as select from Foo { foo, car }; @@ -198,22 +198,26 @@ entity Boo as select from Foo { foo, car }; A `SELECT * from Bar` would result into the same as a query of `Boo`: -```sql -SELECT * from Bar --> { foo, car } -SELECT * from Boo --> { foo, car } +```cds live [FooBar] +SELECT * from Bar //> { foo, car } +``` +```cds live [FooBar] +SELECT * from Boo //> { foo, car } ``` Now, assume a consumer of that package extends the definitions as follows: -```cds +```cds [FooBarBoo] extend Foo with { boo : String; } ``` With that, queries on `Bar` and `Boo` would return different results: -```sql -SELECT * from Bar --> { foo, car, boo } -SELECT * from Boo --> { foo, car } +```cds live [FooBarBoo] +SELECT * from Bar //> { foo, car, boo } +``` +```cds live [FooBarBoo] +SELECT * from Boo //> { foo, car } ``` From 19ea862267e835c441754e5de42089ac618a016a Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Tue, 4 Aug 2026 13:45:52 +0200 Subject: [PATCH 3/4] Show model next to query --- .../restoreCodeGroupPreferences.js | 4 + .../lib/code-groups/useCodeGroupSync.ts | 12 +++ .../components/cds-playground/LiveCode.vue | 86 ++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js b/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js index 1268e3823b..f7cd50bad1 100644 --- a/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js +++ b/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js @@ -44,6 +44,10 @@ if (tabs.length === 0) return + // Skip code groups unrelated to the OS/runtime/cloud-runtime dimensions (e.g. file-path + // tabs), otherwise they'd be forced back to their first tab on every re-init. + if (!tabs.some((tab) => getTabDimension(tab))) return // eslint-disable-line no-undef + const selectedTab = getBestTab(tabs, activeTabs) // eslint-disable-line no-undef const selectedIndex = tabs.indexOf(selectedTab) diff --git a/.vitepress/lib/code-groups/useCodeGroupSync.ts b/.vitepress/lib/code-groups/useCodeGroupSync.ts index 8efdbbd3c7..f2a9544e1a 100644 --- a/.vitepress/lib/code-groups/useCodeGroupSync.ts +++ b/.vitepress/lib/code-groups/useCodeGroupSync.ts @@ -12,6 +12,7 @@ import { addActiveTab, getActiveTabsByDimension, getBestTab, + getTabDimension, setActiveTab, tabsMatch } from './shared.js' @@ -47,6 +48,11 @@ function findCodeGroups(): CodeGroupInfo[] { function applyPreference(codeGroup: CodeGroupInfo): void { const { element, tabs } = codeGroup + + // Skip code groups unrelated to the OS/runtime/cloud-runtime dimensions (e.g. file-path + // tabs), otherwise they'd be forced back to their first tab on every re-init. + if (!tabs.some((tab) => getTabDimension(tab))) return + const selectedTab = getBestTab( tabs, getActiveTabsByDimension((window as any).__CODE_GROUP_ACTIVE_TABS__) @@ -88,6 +94,12 @@ function handleDocumentClick(event: Event): void { const tabLabel = (label.textContent || '').trim() if (!tabLabel) return + // Only tabs that belong to a recognized dimension (OS/runtime/cloud-runtime) should be + // synced across the page. Otherwise unrelated code groups sharing a "/" path segment + // (e.g. "srv/admin-service.cds" vs. "srv/cat-service.cds") get fuzzy-matched and forced + // into the wrong active tab. + if (!getTabDimension(tabLabel)) return + const clickedRect = label.getBoundingClientRect() syncTabs(tabLabel) diff --git a/.vitepress/theme/components/cds-playground/LiveCode.vue b/.vitepress/theme/components/cds-playground/LiveCode.vue index d10b51179f..fdd63df56c 100644 --- a/.vitepress/theme/components/cds-playground/LiveCode.vue +++ b/.vitepress/theme/components/cds-playground/LiveCode.vue @@ -28,6 +28,14 @@
+
@@ -56,15 +64,40 @@
+ +
+
+ +
+ +
+
+ + {{ tab.kind }} + +
+
+
+