@cap-js/db-service 1.6.2 → 1.6.4

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,25 @@
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.6.4](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.3...db-service-v1.6.4) (2024-02-28)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **`cqn2sql`:** smart quoting also for update statements ([#475](https://github.com/cap-js/cds-dbs/issues/475)) ([1688f77](https://github.com/cap-js/cds-dbs/commit/1688f77158c2df37673e969074f1b6d210267336))
13
+ * `INSERT` with first `undefined` value ([#484](https://github.com/cap-js/cds-dbs/issues/484)) ([c21e3c4](https://github.com/cap-js/cds-dbs/commit/c21e3c44140c44ff6378d1fdac32869d9c1c988c))
14
+ * Allow SELECT.join queries again with full infer call ([#469](https://github.com/cap-js/cds-dbs/issues/469)) ([5329ec0](https://github.com/cap-js/cds-dbs/commit/5329ec0a25036a1e42513e8bb9347b0ff8c7aa2d))
15
+ * optimize foreign key access in a join relevant path ([#481](https://github.com/cap-js/cds-dbs/issues/481)) ([5e30de4](https://github.com/cap-js/cds-dbs/commit/5e30de439b62167c4b6d487c4d5cda4f2f0a806d)), closes [#479](https://github.com/cap-js/cds-dbs/issues/479)
16
+
17
+ ## [1.6.3](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.2...db-service-v1.6.3) (2024-02-20)
18
+
19
+
20
+ ### Fixed
21
+
22
+ * **`cqn4sql`:** be robust against `$self.<element>;` references ([#471](https://github.com/cap-js/cds-dbs/issues/471)) ([2921b0e](https://github.com/cap-js/cds-dbs/commit/2921b0e8ada33b172a001d89904893268e751efd))
23
+ * **`infer`:** Always use srv.model ([#451](https://github.com/cap-js/cds-dbs/issues/451)) ([41cf4a2](https://github.com/cap-js/cds-dbs/commit/41cf4a24cf2f5e2411be0dc647af6eb628a6d312))
24
+ * Throw 'new Error' instead of string on $search with multiple words ([#472](https://github.com/cap-js/cds-dbs/issues/472)) ([51be94d](https://github.com/cap-js/cds-dbs/commit/51be94d2333b4a4007f354c805d1b974b19d6d2d))
25
+
7
26
  ## [1.6.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.1...db-service-v1.6.2) (2024-02-16)
8
27
 
9
28
 
package/lib/SQLService.js CHANGED
@@ -114,6 +114,9 @@ class SQLService extends DatabaseService {
114
114
  * @type {Handler}
115
115
  */
116
116
  async onSELECT({ query, data }) {
117
+ if (!query.target) {
118
+ try { this.infer(query) } catch (e) { /**/ }
119
+ }
117
120
  if (query.target && !query.target._unresolved) {
118
121
  // Will return multiple rows with objects inside
119
122
  query.SELECT.expand = 'root'
@@ -21,7 +21,7 @@ const StandardFunctions = {
21
21
  * @returns {string}
22
22
  */
23
23
  search: function (ref, arg) {
24
- if (!('val' in arg)) throw `Only single value arguments are allowed for $search`
24
+ if (!('val' in arg)) throw new Error(`Only single value arguments are allowed for $search`)
25
25
  const refs = ref.list || [ref],
26
26
  { toString } = ref
27
27
  return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
package/lib/cqn2sql.js CHANGED
@@ -31,6 +31,7 @@ class CQN2SQLRenderer {
31
31
  this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
32
32
  this.class = new.target // for IntelliSense
33
33
  this.class._init() // is a noop for subsequent calls
34
+ this.model = srv?.model
34
35
  }
35
36
 
36
37
  static _add_mixins(aspect, mixins) {
@@ -94,6 +95,10 @@ class CQN2SQLRenderer {
94
95
  return q.target ? q : cds_infer(q)
95
96
  }
96
97
 
98
+ cqn4sql(q) {
99
+ return cqn4sql(q, this.model)
100
+ }
101
+
97
102
  // CREATE Statements ------------------------------------------------
98
103
 
99
104
  /**
@@ -108,8 +113,8 @@ class CQN2SQLRenderer {
108
113
  delete this.values
109
114
  this.sql =
110
115
  !query || target['@cds.persistence.table']
111
- ? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
112
- : `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
116
+ ? `CREATE TABLE ${this.quote(name)} ( ${this.CREATE_elements(target.elements)} )`
117
+ : `CREATE VIEW ${this.quote(name)} AS ${this.SELECT(this.cqn4sql(query))}`
113
118
  this.values = []
114
119
  return
115
120
  }
@@ -460,10 +465,11 @@ class CQN2SQLRenderer {
460
465
 
461
466
  let sepsub = ''
462
467
  for (const key in row) {
468
+ let val = row[key]
469
+ if (val === undefined) continue
463
470
  const keyJSON = `${sepsub}${JSON.stringify(key)}:`
464
471
  if (!sepsub) sepsub = ','
465
472
 
466
- let val = row[key]
467
473
  if (val instanceof Readable) {
468
474
  buffer += `${keyJSON}"`
469
475
 
@@ -479,7 +485,6 @@ class CQN2SQLRenderer {
479
485
 
480
486
  buffer += '"'
481
487
  } else {
482
- if (val === undefined) continue
483
488
  if (elements[key]?.type in BINARY_TYPES) {
484
489
  val = transformBase64(val)
485
490
  }
@@ -612,8 +617,8 @@ class CQN2SQLRenderer {
612
617
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
613
618
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
614
619
  ))
615
- this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
616
- cqn4sql(INSERT.as),
620
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
621
+ this.cqn4sql(INSERT.as),
617
622
  )}`
618
623
  this.entries = [this.values]
619
624
  return this.sql
@@ -684,8 +689,8 @@ class CQN2SQLRenderer {
684
689
  UPDATE(q) {
685
690
  const { entity, with: _with, data, where } = q.UPDATE
686
691
  const elements = q.target?.elements
687
- let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
688
- if (entity.as) sql += ` AS ${entity.as}`
692
+ let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
693
+ if (entity.as) sql += ` AS ${this.quote(entity.as)}`
689
694
 
690
695
  let columns = []
691
696
  if (data) _add(data, val => this.val({ val }))
@@ -819,8 +824,8 @@ class CQN2SQLRenderer {
819
824
  */
820
825
  ref({ ref }) {
821
826
  switch (ref[0]) {
822
- case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
823
- case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.'+ref[1]||'id', param: false }] }) // REVISIT: same here?
827
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
828
+ case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.' + ref[1] || 'id', param: false }] }) // REVISIT: same here?
824
829
  default: return ref.map(r => this.quote(r)).join('.')
825
830
  }
826
831
  }
@@ -988,6 +993,6 @@ const _empty = a => !a || a.length === 0
988
993
  * @param {import('@sap/cds/apis/cqn').Query} q
989
994
  * @param {import('@sap/cds/apis/csn').CSN} m
990
995
  */
991
- module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m)
996
+ module.exports = (q, m) => new CQN2SQLRenderer({ model: m }).render(cqn4sql(q, m))
992
997
  module.exports.class = CQN2SQLRenderer
993
998
  module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word
package/lib/cqn4sql.js CHANGED
@@ -43,7 +43,7 @@ const { pseudos } = require('./infer/pseudos')
43
43
  * @param {object} model
44
44
  * @returns {object} transformedQuery the transformed query
45
45
  */
46
- function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
46
+ function cqn4sql(originalQuery, model) {
47
47
  const inferred = infer(originalQuery, model)
48
48
  if (originalQuery.SELECT?.from.args && !originalQuery.joinTree) return inferred
49
49
 
@@ -122,7 +122,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
122
122
  primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
123
123
  })
124
124
 
125
- const transformedSubquery = cqn4sql(subquery)
125
+ const transformedSubquery = cqn4sql(subquery, model)
126
126
 
127
127
  // replace where condition of original query with the transformed subquery
128
128
  // correlate UPDATE / DELETE query with subquery by primary key matches
@@ -838,9 +838,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
838
838
  const calcElement = resolveCalculatedElement(col, true)
839
839
  res.push(calcElement)
840
840
  } else if (col.isJoinRelevant) {
841
- const tableAlias$refLink = getQuerySourceName(col)
841
+ const tableAlias = getQuerySourceName(col)
842
+ const name = calculateElementName(col)
842
843
  const transformedColumn = {
843
- ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
844
+ ref: [tableAlias, name],
844
845
  }
845
846
  if (col.sort) transformedColumn.sort = col.sort
846
847
  if (col.nulls) transformedColumn.nulls = col.nulls
@@ -918,7 +919,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
918
919
  if (q.SELECT.from.uniqueSubqueryAlias) return
919
920
  const last = q.SELECT.from.ref.at(-1)
920
921
  const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
921
- getLastStringSegment(last.id||last),
922
+ getLastStringSegment(last.id || last),
922
923
  originalQuery.outerQueries,
923
924
  )
924
925
  Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
@@ -976,8 +977,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
976
977
  */
977
978
  function isManagedAssocInFlatMode(e) {
978
979
  return (
979
- e.isAssociation && e.keys
980
- && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
980
+ e.isAssociation && e.keys && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
981
981
  )
982
982
  }
983
983
  }
@@ -1038,7 +1038,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1038
1038
  let leafAssoc
1039
1039
  let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
1040
1040
  if (isWildcard && element.type === 'cds.LargeBinary') return []
1041
- if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
1041
+ if (element.on && !element.keys)
1042
+ return [] // unmanaged doesn't make it into columns
1042
1043
  else if (element.virtual === true) return []
1043
1044
  else if (!isJoinRelevant && flatName) baseName = flatName
1044
1045
  else if (isJoinRelevant) {
@@ -1046,10 +1047,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1046
1047
  leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
1047
1048
  let elements
1048
1049
  elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
1049
- if (elements && leaf.alias in elements) {
1050
+ if (elements && leaf.definition.name in elements) {
1050
1051
  element = leafAssoc.definition
1051
1052
  baseName = getFullName(leafAssoc.definition)
1052
- columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
1053
+ columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
1053
1054
  } else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
1054
1055
  } else if (!baseName && structsAreUnfoldedAlready) {
1055
1056
  baseName = element.name // name is already fully constructed
@@ -1127,8 +1128,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1127
1128
  } else {
1128
1129
  // leaf reached
1129
1130
  let flatColumn
1130
- if (columnAlias) flatColumn = { ref: [fkBaseName], as: `${columnAlias}_${fk.ref.join('_')}` }
1131
- else flatColumn = { ref: [fkBaseName] }
1131
+ if (columnAlias) {
1132
+ // if the column has an explicit alias AND the orignal ref
1133
+ // directly resolves to the foreign key, we must not append the fk name to the column alias
1134
+ // e.g. `assoc.fk as FOO` => columns.alias = FOO
1135
+ // `assoc as FOO` => columns.alias = FOO_fk
1136
+ let columnAliasWithFlatFk
1137
+ if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
1138
+ columnAliasWithFlatFk = `${columnAlias}_${fk.as || fk.ref.join('_')}`
1139
+ flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
1140
+ } else flatColumn = { ref: [fkBaseName] }
1132
1141
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
1133
1142
 
1134
1143
  // in a flat model, we must assign the foreign key rather than the key in the target
@@ -1276,7 +1285,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1276
1285
  }
1277
1286
  } else {
1278
1287
  const { list } = token
1279
- if (list.every(e => e.val)) // no need for transformation
1288
+ if (list.every(e => e.val))
1289
+ // no need for transformation
1280
1290
  transformedTokenStream.push({ list })
1281
1291
  else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
1282
1292
  }
@@ -1352,11 +1362,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1352
1362
  // in that case, we have a baseLink `books` which we need to resolve the following steps
1353
1363
  // however, the correct table alias has been assigned to the `author` step
1354
1364
  // hence we need to ignore the alias of the `$baseLink`
1355
- const refHasOwnAssoc =
1365
+ const lastAssoc =
1356
1366
  token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
1357
- const tableAlias = getQuerySourceName(token, refHasOwnAssoc || $baseLink)
1358
- if ((!$baseLink || refHasOwnAssoc) && token.isJoinRelevant) {
1359
- result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
1367
+ const tableAlias = getQuerySourceName(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
1368
+ if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
1369
+ let name = calculateElementName(token, getFullName)
1370
+ result.ref = [tableAlias, name]
1360
1371
  } else if (tableAlias) {
1361
1372
  result.ref = [tableAlias, token.flatName]
1362
1373
  } else {
@@ -1774,13 +1785,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1774
1785
  // naive assumption: if first step is the association itself, all following ref steps must be resolvable
1775
1786
  // within target `assoc.assoc.fk` -> `assoc.assoc_fk`
1776
1787
  else if (
1777
- lhs.$refLinks[0].definition ===
1788
+ lhs.$refLinks[0]?.definition ===
1778
1789
  getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1779
1790
  )
1780
1791
  result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
1781
1792
  // naive assumption: if the path starts with an association which is not the association from
1782
1793
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1783
- else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
1794
+ else if (lhs.$refLinks[0]?.definition.target) result[i].ref = [result[i].ref.join('_')]
1784
1795
  }
1785
1796
  }
1786
1797
  if (backlink) {
@@ -1813,8 +1824,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1813
1824
  result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
1814
1825
  i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
1815
1826
  } else if (lhs.ref) {
1816
- if (lhs.ref[0] === '$self') result[i].ref.splice(0, 1, targetSideRefLink.alias)
1817
- else if (lhs.ref.length > 1) {
1827
+ if (lhs.ref[0] === '$self') {
1828
+ // $self in ref of length > 1
1829
+ // if $self is followed by association, the alias of the association must be used
1830
+ if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
1831
+ // otherwise $self is replaced by the alias of the entity
1832
+ else result[i].ref.splice(0, 1, targetSideRefLink.alias)
1833
+ } else if (lhs.ref.length > 1) {
1818
1834
  if (
1819
1835
  !(lhs.ref[0] in pseudos.elements) &&
1820
1836
  lhs.ref[0] !== assocRefLink.alias &&
@@ -1826,7 +1842,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1826
1842
  // first step is the association itself -> use it's name as it becomes the table alias
1827
1843
  result[i].ref.splice(0, 1, assocRefLink.alias)
1828
1844
  } else if (
1829
- definition.name in (targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
1845
+ definition.name in
1846
+ (targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
1830
1847
  ) {
1831
1848
  // first step is association which refers to its foreign key by dot notation
1832
1849
  result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
@@ -1991,21 +2008,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1991
2008
  return model.definitions[assoc.target] || null
1992
2009
  }
1993
2010
 
1994
- /**
1995
- * Calculate the flat name for a deeply nested element:
1996
- * @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
1997
- *
1998
- * @param {CSN.element} node an element
1999
- * @param {object} name the last part of the name, e.g. the name of the deeply nested element
2000
- * @returns the flat name of the element
2001
- */
2002
- function getFullName(node, name = node.name) {
2003
- // REVISIT: this is an unfortunate implementation
2004
- if (!node.parent || node.parent.kind === 'entity') return name
2005
-
2006
- return getFullName(node.parent, `${node.parent.name}_${name}`)
2007
- }
2008
-
2009
2011
  /**
2010
2012
  * Calculates the name of the source which can be used to address the given node.
2011
2013
  *
@@ -2074,6 +2076,31 @@ module.exports = Object.assign(cqn4sql, {
2074
2076
  notSupportedOps,
2075
2077
  })
2076
2078
 
2079
+ function calculateElementName(token) {
2080
+ const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess)
2081
+ let name
2082
+ if (nonJoinRelevantAssoc)
2083
+ // calculate fk name
2084
+ name = token.ref.slice(nonJoinRelevantAssoc).join('_')
2085
+ else name = token.$refLinks[token.$refLinks.length - 1].definition.name
2086
+ return name
2087
+ }
2088
+
2089
+ /**
2090
+ * Calculate the flat name for a deeply nested element:
2091
+ * @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
2092
+ *
2093
+ * @param {CSN.element} node an element
2094
+ * @param {object} name the last part of the name, e.g. the name of the deeply nested element
2095
+ * @returns the flat name of the element
2096
+ */
2097
+ function getFullName(node, name = node.name) {
2098
+ // REVISIT: this is an unfortunate implementation
2099
+ if (!node.parent || node.parent.kind === 'entity') return name
2100
+
2101
+ return getFullName(node.parent, `${node.parent.name}_${name}`)
2102
+ }
2103
+
2077
2104
  function copy(obj) {
2078
2105
  const walk = function (par, prop) {
2079
2106
  const val = prop ? par[prop] : par
@@ -22,7 +22,7 @@ for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
22
22
  * @param {import('@sap/cds/apis/csn').CSN} [model]
23
23
  * @returns {import('./cqn').Query} = q with .target and .elements
24
24
  */
25
- function infer(originalQuery, model = cds.context?.model || cds.model) {
25
+ function infer(originalQuery, model) {
26
26
  if (!model) throw new Error('Please specify a model')
27
27
  const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
28
28
 
@@ -181,6 +181,7 @@ class JoinTree {
181
181
  col.$refLinks[i].alias = node.$refLink.alias
182
182
  col.$refLinks[i].definition = node.$refLink.definition
183
183
  col.$refLinks[i].target = node.$refLink.target
184
+ col.$refLinks[i].onlyForeignKeyAccess = node.$refLink.onlyForeignKeyAccess
184
185
  } else {
185
186
  if (col.expand && !col.ref[i + 1]) {
186
187
  node.$refLink.onlyForeignKeyAccess = false
@@ -201,7 +202,7 @@ class JoinTree {
201
202
  const elements =
202
203
  node.$refLink?.definition.isAssociation &&
203
204
  (node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
204
- if (node.$refLink && (!elements || !(child.$refLink.alias in elements)))
205
+ if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements)))
205
206
  // foreign key access
206
207
  node.$refLink.onlyForeignKeyAccess = false
207
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
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": {