@cap-js/db-service 1.6.3 → 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,16 @@
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
+
7
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)
8
18
 
9
19
 
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'
package/lib/cqn2sql.js CHANGED
@@ -113,8 +113,8 @@ class CQN2SQLRenderer {
113
113
  delete this.values
114
114
  this.sql =
115
115
  !query || target['@cds.persistence.table']
116
- ? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
117
- : `CREATE VIEW ${name} AS ${this.SELECT(this.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))}`
118
118
  this.values = []
119
119
  return
120
120
  }
@@ -465,10 +465,11 @@ class CQN2SQLRenderer {
465
465
 
466
466
  let sepsub = ''
467
467
  for (const key in row) {
468
+ let val = row[key]
469
+ if (val === undefined) continue
468
470
  const keyJSON = `${sepsub}${JSON.stringify(key)}:`
469
471
  if (!sepsub) sepsub = ','
470
472
 
471
- let val = row[key]
472
473
  if (val instanceof Readable) {
473
474
  buffer += `${keyJSON}"`
474
475
 
@@ -484,7 +485,6 @@ class CQN2SQLRenderer {
484
485
 
485
486
  buffer += '"'
486
487
  } else {
487
- if (val === undefined) continue
488
488
  if (elements[key]?.type in BINARY_TYPES) {
489
489
  val = transformBase64(val)
490
490
  }
@@ -617,7 +617,7 @@ class CQN2SQLRenderer {
617
617
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
618
618
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
619
619
  ))
620
- this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
620
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
621
621
  this.cqn4sql(INSERT.as),
622
622
  )}`
623
623
  this.entries = [this.values]
@@ -689,8 +689,8 @@ class CQN2SQLRenderer {
689
689
  UPDATE(q) {
690
690
  const { entity, with: _with, data, where } = q.UPDATE
691
691
  const elements = q.target?.elements
692
- let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
693
- 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)}`
694
694
 
695
695
  let columns = []
696
696
  if (data) _add(data, val => this.val({ val }))
package/lib/cqn4sql.js CHANGED
@@ -838,9 +838,10 @@ function cqn4sql(originalQuery, 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
@@ -1046,10 +1047,10 @@ function cqn4sql(originalQuery, 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) {
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
@@ -1353,11 +1362,12 @@ function cqn4sql(originalQuery, model) {
1353
1362
  // in that case, we have a baseLink `books` which we need to resolve the following steps
1354
1363
  // however, the correct table alias has been assigned to the `author` step
1355
1364
  // hence we need to ignore the alias of the `$baseLink`
1356
- const refHasOwnAssoc =
1365
+ const lastAssoc =
1357
1366
  token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
1358
- const tableAlias = getQuerySourceName(token, refHasOwnAssoc || $baseLink)
1359
- if ((!$baseLink || refHasOwnAssoc) && token.isJoinRelevant) {
1360
- 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]
1361
1371
  } else if (tableAlias) {
1362
1372
  result.ref = [tableAlias, token.flatName]
1363
1373
  } else {
@@ -1814,7 +1824,8 @@ function cqn4sql(originalQuery, model) {
1814
1824
  result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
1815
1825
  i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
1816
1826
  } else if (lhs.ref) {
1817
- if (lhs.ref[0] === '$self') { // $self in ref of length > 1
1827
+ if (lhs.ref[0] === '$self') {
1828
+ // $self in ref of length > 1
1818
1829
  // if $self is followed by association, the alias of the association must be used
1819
1830
  if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
1820
1831
  // otherwise $self is replaced by the alias of the entity
@@ -1997,21 +2008,6 @@ function cqn4sql(originalQuery, model) {
1997
2008
  return model.definitions[assoc.target] || null
1998
2009
  }
1999
2010
 
2000
- /**
2001
- * Calculate the flat name for a deeply nested element:
2002
- * @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
2003
- *
2004
- * @param {CSN.element} node an element
2005
- * @param {object} name the last part of the name, e.g. the name of the deeply nested element
2006
- * @returns the flat name of the element
2007
- */
2008
- function getFullName(node, name = node.name) {
2009
- // REVISIT: this is an unfortunate implementation
2010
- if (!node.parent || node.parent.kind === 'entity') return name
2011
-
2012
- return getFullName(node.parent, `${node.parent.name}_${name}`)
2013
- }
2014
-
2015
2011
  /**
2016
2012
  * Calculates the name of the source which can be used to address the given node.
2017
2013
  *
@@ -2080,6 +2076,31 @@ module.exports = Object.assign(cqn4sql, {
2080
2076
  notSupportedOps,
2081
2077
  })
2082
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
+
2083
2104
  function copy(obj) {
2084
2105
  const walk = function (par, prop) {
2085
2106
  const val = prop ? par[prop] : par
@@ -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.3",
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": {