@cap-js/db-service 1.10.2 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [1.10.3](https://github.com/cap-js/cds-dbs/compare/db-service-v1.10.2...db-service-v1.10.3) (2024-07-05)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * rewrite assoc chains if intermediate assoc is not fk ([#715](https://github.com/cap-js/cds-dbs/issues/715)) ([3873f9a](https://github.com/cap-js/cds-dbs/commit/3873f9adce3ff26cafb2b18b9d2115758b0f0830))
13
+ * Support expand with group by clause ([#721](https://github.com/cap-js/cds-dbs/issues/721)) ([90c9e6a](https://github.com/cap-js/cds-dbs/commit/90c9e6a4da9d4a3451ec0ed60dd0815c04600134))
14
+
7
15
  ## [1.10.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.10.1...db-service-v1.10.2) (2024-06-25)
8
16
 
9
17
 
package/lib/cqn2sql.js CHANGED
@@ -222,6 +222,7 @@ class CQN2SQLRenderer {
222
222
  if (distinct) sql += ` DISTINCT`
223
223
  if (!_empty(columns)) sql += ` ${columns}`
224
224
  if (!_empty(from)) sql += ` FROM ${this.from(from)}`
225
+ else sql += this.from_dummy()
225
226
  if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
226
227
  if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
227
228
  if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
@@ -310,12 +311,7 @@ class CQN2SQLRenderer {
310
311
  */
311
312
  column_expr(x, q) {
312
313
  if (x === '*') return '*'
313
- ///////////////////////////////////////////////////////////////////////////////////////
314
- // REVISIT: that should move out of here!
315
- if (x?.element?.['@cds.extension']) {
316
- return `extensions__->${this.string('$."' + x.element.name + '"')} as ${x.as || x.element.name}`
317
- }
318
- ///////////////////////////////////////////////////////////////////////////////////////
314
+
319
315
  let sql = this.expr({ param: false, __proto__: x })
320
316
  let alias = this.column_alias4(x, q)
321
317
  if (alias) sql += ' as ' + this.quote(alias)
@@ -351,6 +347,14 @@ class CQN2SQLRenderer {
351
347
  return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
352
348
  }
353
349
 
350
+ /**
351
+ * Renders a FROM clause for when the query does not have a target
352
+ * @returns {string} SQL
353
+ */
354
+ from_dummy() {
355
+ return ''
356
+ }
357
+
354
358
  /**
355
359
  * Renders a FROM clause into generic SQL
356
360
  * @param {import('./infer/cqn').ref['ref'][0]['args']} args
@@ -480,7 +484,7 @@ class CQN2SQLRenderer {
480
484
  : ObjectKeys(INSERT.entries[0])
481
485
 
482
486
  /** @type {string[]} */
483
- this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true)
487
+ this.columns = columns
484
488
 
485
489
  if (!elements) {
486
490
  this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
@@ -493,24 +497,7 @@ class CQN2SQLRenderer {
493
497
  elements,
494
498
  !!q.UPSERT,
495
499
  )
496
- const extraction = extractions
497
- .map(c => {
498
- const element = elements?.[c.name]
499
- if (element?.['@cds.extension']) {
500
- return false
501
- }
502
- if (c.name === 'extensions__') {
503
- const merges = extractions.filter(c => elements?.[c.name]?.['@cds.extension'])
504
- if (merges.length) {
505
- c.sql = `json_set(ifnull(${c.sql},'{}'),${merges.map(
506
- c => this.string('$."' + c.name + '"') + ',' + c.sql,
507
- )})`
508
- }
509
- }
510
- return c
511
- })
512
- .filter(a => a)
513
- .map(c => c.sql)
500
+ const extraction = extractions.map(c => c.sql)
514
501
 
515
502
  // Include this.values for placeholders
516
503
  /** @type {unknown[][]} */
@@ -783,14 +770,6 @@ class CQN2SQLRenderer {
783
770
  }
784
771
  }
785
772
 
786
- columns = columns.map(c => {
787
- if (q.elements?.[c.name]?.['@cds.extension']) return {
788
- name: 'extensions__',
789
- sql: `jsonb_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
790
- }
791
- return c
792
- })
793
-
794
773
  const extraction = this.managed(columns, elements, true).map(c => `${this.quote(c.name)}=${c.sql}`)
795
774
 
796
775
  sql += ` SET ${extraction}`
package/lib/cqn4sql.js CHANGED
@@ -799,10 +799,20 @@ function cqn4sql(originalQuery, model) {
799
799
  ? column.ref.slice(1).map(idOnly).join('_') // omit explicit table alias from name of column
800
800
  : column.ref.map(idOnly).join('_'))
801
801
 
802
+ // if there is a group by on the main query, all
803
+ // columns of the expand must be in the groupBy
804
+ if (transformedQuery.SELECT.groupBy) {
805
+ const baseRef =
806
+ column.$refLinks[0].definition.SELECT || column.$refLinks[0].definition.kind === 'entity'
807
+ ? column.ref.slice(1)
808
+ : column.ref
809
+
810
+ return _subqueryForGroupBy(column, baseRef, columnAlias)
811
+ }
812
+
802
813
  // we need to respect the aliases of the outer query, so the columnAlias might not be suitable
803
814
  // as table alias for the correlated subquery
804
815
  const uniqueSubqueryAlias = getNextAvailableTableAlias(columnAlias, originalQuery.outerQueries)
805
-
806
816
  // `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
807
817
  const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
808
818
  const subqueryBase = Object.fromEntries(
@@ -815,7 +825,7 @@ function cqn4sql(originalQuery, model) {
815
825
  from,
816
826
  columns: JSON.parse(JSON.stringify(column.expand)),
817
827
  expand: true,
818
- one: column.$refLinks[column.$refLinks.length - 1].definition.is2one,
828
+ one: column.$refLinks.at(-1).definition.is2one,
819
829
  },
820
830
  }
821
831
  const expanded = transformSubquery(subquery)
@@ -851,6 +861,74 @@ function cqn4sql(originalQuery, model) {
851
861
  }
852
862
  return subq
853
863
  }
864
+
865
+ /**
866
+ * Generates a special subquery for the `expand` of the `column`.
867
+ * All columns in the `expand` must be part of the GROUP BY clause of the main query.
868
+ * If this is the case, the subqueries columns match the corresponding references of the group by.
869
+ * Nested expands are also supported.
870
+ *
871
+ * @param {Object} column - To expand.
872
+ * @param {Array} baseRef - The base reference for the expanded column.
873
+ * @param {string} subqueryAlias - The alias of the `expand` subquery column.
874
+ * @returns {Object} - The subquery object.
875
+ * @throws {Error} - If one of the `ref`s in the `column.expand` is not part of the GROUP BY clause.
876
+ */
877
+ function _subqueryForGroupBy(column, baseRef, subqueryAlias) {
878
+ const groupByLookup = new Map(
879
+ transformedQuery.SELECT.groupBy.map(c => [c.ref && c.ref.map(refWithConditions).join('.'), c]),
880
+ )
881
+
882
+ // to be attached to dummy query
883
+ const elements = {}
884
+ const expandedColumns = column.expand.map(expand => {
885
+ const fullRef = [...baseRef, ...expand.ref]
886
+
887
+ if (expand.expand) {
888
+ const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
889
+ elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
890
+ return nested
891
+ }
892
+
893
+ const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
894
+ if (!groupByRef) {
895
+ throw new Error(
896
+ `The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
897
+ )
898
+ }
899
+
900
+ const columnCopy = Object.create(groupByRef)
901
+ if (expand.as) {
902
+ columnCopy.as = expand.as
903
+ }
904
+ if (columnCopy.isJoinRelevant) {
905
+ const tableAlias = getQuerySourceName(columnCopy)
906
+ const name = calculateElementName(columnCopy)
907
+ columnCopy.ref = [tableAlias, name]
908
+ }
909
+ elements[expand.as || expand.ref.map(idOnly).join('_')] = columnCopy.$refLinks.at(-1).definition
910
+ return columnCopy
911
+ })
912
+
913
+ const SELECT = {
914
+ from: null,
915
+ columns: expandedColumns,
916
+ }
917
+ return Object.defineProperties(
918
+ {},
919
+ {
920
+ SELECT: {
921
+ value: Object.defineProperties(SELECT, {
922
+ expand: { value: true, writable: true }, // non-enumerable
923
+ one: { value: column.$refLinks.at(-1).definition.is2one, writable: true }, // non-enumerable
924
+ }),
925
+ enumerable: true,
926
+ },
927
+ as: { value: subqueryAlias, enumerable: true, writable: true },
928
+ elements: { value: elements }, // non-enumerable
929
+ },
930
+ )
931
+ }
854
932
  }
855
933
 
856
934
  function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
@@ -1573,7 +1651,7 @@ function cqn4sql(originalQuery, model) {
1573
1651
  transformedFrom.args.push(f)
1574
1652
  }
1575
1653
  })
1576
- return { transformedFrom }
1654
+ return { transformedFrom, transformedWhere: existingWhere }
1577
1655
  } else if (from.SELECT) {
1578
1656
  transformedFrom = transformSubquery(from)
1579
1657
  if (from.as) {
@@ -1583,7 +1661,7 @@ function cqn4sql(originalQuery, model) {
1583
1661
  // select from anonymous query, use artificial alias
1584
1662
  transformedFrom.as = Object.keys(originalQuery.sources)[0]
1585
1663
  }
1586
- return { transformedFrom }
1664
+ return { transformedFrom, transformedWhere: existingWhere }
1587
1665
  } else {
1588
1666
  return _transformFrom()
1589
1667
  }
@@ -2251,4 +2329,12 @@ function setElementOnColumns(col, element) {
2251
2329
 
2252
2330
  const getName = col => col.as || col.ref?.at(-1)
2253
2331
  const idOnly = ref => ref.id || ref
2332
+ const refWithConditions = step => {
2333
+ let appendix
2334
+ const { args, where } = step
2335
+ if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
2336
+ else if (where) appendix = JSON.stringify(where)
2337
+ else if (args) appendix = JSON.stringify(args)
2338
+ return appendix ? step.id + appendix : step
2339
+ }
2254
2340
  const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
@@ -1013,9 +1013,11 @@ function infer(originalQuery, model) {
1013
1013
  }
1014
1014
  return true
1015
1015
  }
1016
- if (assoc && assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) {
1016
+ if (assoc) {
1017
1017
  // foreign key access without filters never join relevant
1018
- return false
1018
+ if (assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) return false
1019
+ // <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
1020
+ return true
1019
1021
  }
1020
1022
  if (link.definition.target && link.definition.keys) {
1021
1023
  if (column.ref[i + 1] || assoc) fkAccess = false
@@ -1198,6 +1200,8 @@ function infer(originalQuery, model) {
1198
1200
  firstStepIsEntity = true
1199
1201
  if (arg.$refLinks.length === 1) return `${cur.definition.name}`
1200
1202
  return `${cur.definition.name}`
1203
+ } else if (cur.definition.SELECT) {
1204
+ return `${cur.definition.as}`
1201
1205
  }
1202
1206
  const dot = i === 1 && firstStepIsEntity ? ':' : '.' // divide with colon if first step is entity
1203
1207
  return res !== '' ? res + dot + cur.definition.name : cur.definition.name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.10.2",
3
+ "version": "1.10.3",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {