@cap-js/db-service 1.7.0 → 1.8.0

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,24 @@
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.8.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.7.0...db-service-v1.8.0) (2024-04-12)
8
+
9
+
10
+ ### Added
11
+
12
+ * Odata built-in query functions ([#558](https://github.com/cap-js/cds-dbs/issues/558)) ([6e63367](https://github.com/cap-js/cds-dbs/commit/6e6336757129c4a9dac56f93fd768bb41d071c46))
13
+ * support HANA stored procedures ([#542](https://github.com/cap-js/cds-dbs/issues/542)) ([52a00a0](https://github.com/cap-js/cds-dbs/commit/52a00a0d642ba3c58dcad97b3ea1456f1bf3b04a))
14
+
15
+
16
+ ### Fixed
17
+
18
+ * **`expand`:** Only accept on structures, assocs or table aliases ([#551](https://github.com/cap-js/cds-dbs/issues/551)) ([3248512](https://github.com/cap-js/cds-dbs/commit/32485129147cd1b376f1d2faf2ea7c7232ba3794))
19
+ * **`order by`:** for localized sorting, prepend table alias ([#546](https://github.com/cap-js/cds-dbs/issues/546)) ([a273a92](https://github.com/cap-js/cds-dbs/commit/a273a9278b2551ed3381795effe28cf8de41b1bd))
20
+ * etag with stream_compat ([#562](https://github.com/cap-js/cds-dbs/issues/562)) ([b0a3a41](https://github.com/cap-js/cds-dbs/commit/b0a3a418fbcff7eb7e7b8fa4ff031e1c0c0faac4))
21
+ * exclude `cds.LargeBinary` from wildcard expansion ([#577](https://github.com/cap-js/cds-dbs/issues/577)) ([6661d63](https://github.com/cap-js/cds-dbs/commit/6661d635b2895a13d47e42495acf6fbd7247c535))
22
+ * Reduce insert queries for deep update ([#568](https://github.com/cap-js/cds-dbs/issues/568)) ([55e5114](https://github.com/cap-js/cds-dbs/commit/55e511471743c0445d41e8297f5530abe167a270))
23
+ * Reduced count query complexity when possible ([#553](https://github.com/cap-js/cds-dbs/issues/553)) ([3331f02](https://github.com/cap-js/cds-dbs/commit/3331f0224f02bd2e6cc9c6d2cd5f1c37a36ec8dd))
24
+
7
25
  ## [1.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.4...db-service-v1.7.0) (2024-03-22)
8
26
 
9
27
 
package/lib/SQLService.js CHANGED
@@ -134,7 +134,15 @@ class SQLService extends DatabaseService {
134
134
  if (cds.env.features.stream_compat) {
135
135
  if (query._streaming) {
136
136
  this._changeToStreams(cqn.SELECT.columns, rows, true, true)
137
- return rows.length ? { value: Object.values(rows[0])[0] } : undefined
137
+ if (!rows.length) return
138
+
139
+ const result = rows[0]
140
+ // stream is always on position 0. Further properties like etag are inserted later.
141
+ let [key, val] = Object.entries(result)[0]
142
+ result.value = val
143
+ delete result[key]
144
+
145
+ return result
138
146
  }
139
147
  } else {
140
148
  this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
@@ -265,7 +273,7 @@ class SQLService extends DatabaseService {
265
273
  * @param {string} sql
266
274
  */
267
275
  hasResults(sql) {
268
- return /^(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
276
+ return /^\s*(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
269
277
  }
270
278
 
271
279
  /**
@@ -281,17 +289,23 @@ class SQLService extends DatabaseService {
281
289
  const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
282
290
  if (max === undefined || (n < max && (n || !offset))) return n + offset
283
291
  }
284
- // REVISIT: made uppercase count because of HANA reserved word quoting
285
- const cq = SELECT.one([{ func: 'count', as: 'COUNT' }]).from(
292
+
293
+ // Keep original query columns when potentially used insde conditions
294
+ const { having, groupBy } = query.SELECT
295
+ const columns = (having?.length || groupBy?.length)
296
+ ? query.SELECT.columns.filter(c => !c.expand)
297
+ : [{ val: 1 }]
298
+ const cq = SELECT.one([{ func: 'count' }]).from(
286
299
  cds.ql.clone(query, {
300
+ columns,
287
301
  localized: false,
288
302
  expand: false,
289
303
  limit: undefined,
290
304
  orderBy: undefined,
291
305
  }),
292
306
  )
293
- const { count, COUNT } = await this.onSELECT({ query: cq })
294
- return count ?? COUNT
307
+ const { count } = await this.onSELECT({ query: cq })
308
+ return count
295
309
  }
296
310
 
297
311
  /**
@@ -152,6 +152,13 @@ const StandardFunctions = {
152
152
  current_time: p => (p ? `current_time(${p})` : 'current_time'),
153
153
  current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
154
154
 
155
+ /**
156
+ * Generates SQL statement that produces current point in time (date and time with time zone)
157
+ * @returns {string}
158
+ */
159
+ now: function() {
160
+ return this.session_context({val: '$now'})
161
+ },
155
162
  /**
156
163
  * Generates SQL statement that produces the year of a given timestamp
157
164
  * @param {string} x
@@ -189,29 +196,27 @@ const StandardFunctions = {
189
196
  * /
190
197
  second: x => `cast( strftime('%S',${x}) as Integer )`,
191
198
 
199
+ // REVISIT: make precision configurable
192
200
  /**
193
201
  * Generates SQL statement that produces the fractional seconds of a given timestamp
194
202
  * @param {string} x
195
203
  * @returns {string}
196
204
  */
197
- fractionalseconds: x => `cast( strftime('%f0000',${x}) as Integer )`,
205
+ fractionalseconds: x => `cast( substr( strftime('%f', ${x}), length(strftime('%f', ${x})) - 3) as REAL)`,
198
206
 
199
207
  /**
200
208
  * maximum date time value
201
209
  * @returns {string}
202
210
  */
203
- maxdatetime: () => '9999-12-31 23:59:59.999',
211
+ maxdatetime: () => "'9999-12-31T23:59:59.999Z'",
204
212
  /**
205
213
  * minimum date time value
206
214
  * @returns {string}
207
215
  */
208
- mindatetime: () => '0001-01-01 00:00:00.000',
216
+ mindatetime: () => "'0001-01-01T00:00:00.000Z'",
209
217
 
210
218
  // odata spec defines the date time offset type as a normal ISO time stamp
211
219
  // Where the timezone can either be 'Z' (for UTC) or [+|-]xx:xx for the time offset
212
- // sqlite understands this so by splitting the timezone from the actual date
213
- // prefixing it with 1970 it allows sqlite to give back the number of seconds
214
- // which can be divided by 60 back to minutes
215
220
  /**
216
221
  * Generates SQL statement that produces the offset in minutes of a given date time offset string
217
222
  * @param {string} x
@@ -219,7 +224,9 @@ const StandardFunctions = {
219
224
  */
220
225
  totaloffsetminutes: x => `case
221
226
  when substr(${x}, length(${x})) = 'z' then 0
222
- else strftime('%s', '1970-01-01T00:00:00' || substr(${x}, length(${x}) - 5)) / 60
227
+ else sign( cast( substr(${x}, length(${x}) - 5) as Integer )) *
228
+ ( cast( strftime('%H', substr(${x}, length(${x}) - 4 )) as Integer ) * 60 +
229
+ cast( strftime('%M', substr(${x},length(${x}) - 4 )) as Integer ))
223
230
  end`,
224
231
 
225
232
  // odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
package/lib/cqn4sql.js CHANGED
@@ -854,7 +854,7 @@ function cqn4sql(originalQuery, model) {
854
854
  function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
855
855
  const res = []
856
856
  for (let i = 0; i < columns.length; i++) {
857
- const col = columns[i]
857
+ let col = columns[i]
858
858
  if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
859
859
  const calcElement = resolveCalculatedElement(col, true)
860
860
  res.push(calcElement)
@@ -877,8 +877,37 @@ function cqn4sql(originalQuery, model) {
877
877
  res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
878
878
  continue
879
879
  }
880
- const { target } = col.$refLinks[0]
881
- const tableAlias = target.SELECT ? null : getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
880
+ const { target, definition } = col.$refLinks[0]
881
+ let tableAlias = null
882
+ if (target.SELECT?.columns && inOrderBy) {
883
+ // usually TA is omitted if order by ref is a column
884
+ // if a localized sorting is requested, we add `COLLATE`s
885
+ // later on, which transforms the simple name to an expression
886
+ // --> in an expression, only source elements can be addressed, hence we must add TA
887
+ if (target.SELECT.localized && definition.type === 'cds.String') {
888
+ const referredCol = target.SELECT.columns.find(c => {
889
+ return c.as === col.ref[0] || c.ref?.at(-1) === col.ref[0]
890
+ })
891
+ if (referredCol) {
892
+ // keep sort and nulls properties
893
+ referredCol.sort = col.sort
894
+ referredCol.nulls = col.nulls
895
+ col = referredCol
896
+ if (definition.kind === 'element') {
897
+ tableAlias = getQuerySourceName(col)
898
+ } else {
899
+ // we must replace the reference with the underlying expression
900
+ const { val, func, args, xpr } = col
901
+ if (val) res.push({ val })
902
+ if (func) res.push({ func, args })
903
+ if (xpr) res.push({ xpr })
904
+ continue
905
+ }
906
+ }
907
+ }
908
+ } else {
909
+ tableAlias = getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
910
+ }
882
911
  const leaf = col.$refLinks[col.$refLinks.length - 1].definition
883
912
  if (leaf.virtual === true) continue // already in getFlatColumnForElement
884
913
  let baseName
@@ -972,7 +1001,7 @@ function cqn4sql(originalQuery, model) {
972
1001
  const { index, tableAlias } = inferred.$combinedElements[k][0]
973
1002
  const element = tableAlias.elements[k]
974
1003
  // ignore FK for odata csn / ignore blobs from wildcard expansion
975
- if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
1004
+ if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') return
976
1005
  // for wildcard on subquery in from, just reference the elements
977
1006
  if (tableAlias.SELECT && !element.elements && !element.target) {
978
1007
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
@@ -242,10 +242,23 @@ const _getDeepQueries = (diff, target, root = false) => {
242
242
  queries.push(...subQueries)
243
243
  }
244
244
 
245
- queries.forEach(q => {
245
+ const insertQueries = new Map()
246
+
247
+ return queries.map(q => {
248
+ // Merge all INSERT statements for each target
249
+ if (q.INSERT) {
250
+ const target = q.target
251
+ if (insertQueries.has(target)) {
252
+ insertQueries.get(target).INSERT.entries.push(...q.INSERT.entries)
253
+ return
254
+ } else {
255
+ insertQueries.set(target, q)
256
+ }
257
+ }
246
258
  Object.defineProperty(q, handledDeep, { value: true })
259
+ return q
247
260
  })
248
- return queries
261
+ .filter(a => a)
249
262
  }
250
263
 
251
264
  module.exports = {
@@ -727,6 +727,9 @@ function infer(originalQuery, model) {
727
727
  function resolveInline(col, namePrefix = col.as || col.flatName) {
728
728
  const { inline, $refLinks } = col
729
729
  const $leafLink = $refLinks[$refLinks.length - 1]
730
+ if(!$leafLink.definition.target && !$leafLink.definition.elements) {
731
+ throw new Error(`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
732
+ }
730
733
  let elements = {}
731
734
  inline.forEach(inlineCol => {
732
735
  inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true, baseColumn: col })
@@ -780,6 +783,9 @@ function infer(originalQuery, model) {
780
783
  function resolveExpand(col) {
781
784
  const { expand, $refLinks } = col
782
785
  const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
786
+ if(!$leafLink.definition.target && !$leafLink.definition.elements) {
787
+ throw new Error(`Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
788
+ }
783
789
  const target = getDefinition($leafLink.definition.target)
784
790
  if (target) {
785
791
  const expandSubquery = {
@@ -795,19 +801,20 @@ function infer(originalQuery, model) {
795
801
  ? new cds.struct({ elements: inferredExpandSubquery.elements })
796
802
  : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
797
803
  return Object.defineProperty(res, '$assocExpand', { value: true })
798
- } // struct
799
- let elements = {}
800
- expand.forEach(e => {
801
- if (e === '*') {
802
- elements = { ...elements, ...$leafLink.definition.elements }
803
- } else {
804
- inferQueryElement(e, false, $leafLink, { inExpr: true, inNestedProjection: true })
805
- if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
806
- if (e.inline) elements = { ...elements, ...resolveInline(e) }
807
- else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
808
- }
809
- })
810
- return new cds.struct({ elements })
804
+ } else if ($leafLink.definition.elements) {
805
+ let elements = {}
806
+ expand.forEach(e => {
807
+ if (e === '*') {
808
+ elements = { ...elements, ...$leafLink.definition.elements }
809
+ } else {
810
+ inferQueryElement(e, false, $leafLink, { inExpr: true, inNestedProjection: true })
811
+ if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
812
+ if (e.inline) elements = { ...elements, ...resolveInline(e) }
813
+ else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
814
+ }
815
+ })
816
+ return new cds.struct({ elements })
817
+ }
811
818
  }
812
819
 
813
820
  function stepNotFoundInPredecessor(step, def) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
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": {