@cap-js/db-service 1.17.1 → 1.18.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,28 @@
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.18.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.17.2...db-service-v1.18.0) (2025-03-04)
8
+
9
+
10
+ ### Added
11
+
12
+ * query modifiers on expand `ref` are propagated to subquery ([#1049](https://github.com/cap-js/cds-dbs/issues/1049)) ([39fbadf](https://github.com/cap-js/cds-dbs/commit/39fbadf25a874f810ac2795f2e6b0a46c3678058))
13
+ * support query modifiers at leaf of from ref ([#1050](https://github.com/cap-js/cds-dbs/issues/1050)) ([500a666](https://github.com/cap-js/cds-dbs/commit/500a666a9a054dd72d6ec8ccba0c6a6ddc263cd3))
14
+
15
+
16
+ ### Fixed
17
+
18
+ * `<expand>[@odata](https://github.com/odata).count` queries ([#966](https://github.com/cap-js/cds-dbs/issues/966)) ([6607a84](https://github.com/cap-js/cds-dbs/commit/6607a8404aa70f2f3f7c6c65c7e9b1c324a5230b))
19
+ * align debug log format of stmt values ([#1052](https://github.com/cap-js/cds-dbs/issues/1052)) ([93af0fe](https://github.com/cap-js/cds-dbs/commit/93af0fe5f93a0c1b91f592417b31fdb6266fdd79))
20
+ * expand + groupby may return null, dont attach `.element` ([#1042](https://github.com/cap-js/cds-dbs/issues/1042)) ([cf2e0a2](https://github.com/cap-js/cds-dbs/commit/cf2e0a215e89f9055e28d9f0984adf292e220aee))
21
+
22
+ ## [1.17.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.17.1...db-service-v1.17.2) (2025-02-09)
23
+
24
+
25
+ ### Fixed
26
+
27
+ * replace polynomial regex with simple string op ([#1015](https://github.com/cap-js/cds-dbs/issues/1015)) ([3fe6e6b](https://github.com/cap-js/cds-dbs/commit/3fe6e6b7f7aaf5aafb811acf2838cd1da30052a8))
28
+
7
29
  ## [1.17.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.17.0...db-service-v1.17.1) (2025-02-04)
8
30
 
9
31
 
@@ -1,30 +1,16 @@
1
- const cds = require("@sap/cds")
1
+ 'use strict'
2
2
 
3
+ // OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
3
4
  const StandardFunctions = {
4
- // OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
5
-
6
- // String and Collection Functions
7
- /**
8
- * Generates SQL statement that produces the length of a given string
9
- * @param {string} x
10
- * @returns {string}
11
- */
12
- length: x => `length(${x})`,
13
- /**
14
- * Generates SQL statement that produces the average of a given expression
15
- * @param {string} x
16
- * @returns {string}
17
- */
18
- average: x => `avg(${x})`,
19
5
  /**
20
6
  * Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns
21
- * @param {string} ref
22
- * @param {string} arg
23
- * @returns {string}
7
+ * @param {string} ref - The reference object containing column information
8
+ * @param {string} arg - The argument object containing the search value
9
+ * @returns {string} - SQL statement
24
10
  */
25
11
  search: function (ref, arg) {
26
- if (!('val' in arg)) throw new Error(`Only single value arguments are allowed for $search`)
27
- // only apply first search term, rest is ignored
12
+ if (!('val' in arg)) throw new Error('Only single value arguments are allowed for $search')
13
+ // Only apply first search term, rest is ignored
28
14
  const sub = /("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/.exec(arg.val)
29
15
  let val
30
16
  try {
@@ -37,53 +23,50 @@ const StandardFunctions = {
37
23
  const { toString } = ref
38
24
  return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
39
25
  },
40
- /**
41
- * Generates SQL statement that produces a string with all provided strings concatenated
42
- * @param {...string} args
43
- * @returns {string}
44
- */
45
- concat: (...args) => args.map(a => (a.xpr ? `(${a})` : a)).join(' || '),
26
+
27
+ // ==============================
28
+ // Aggregation Functions
29
+ // ==============================
46
30
 
47
31
  /**
48
- * Generates SQL statement that produces a boolean value indicating whether the first string contains the second string
49
- * @param {...string} args
50
- * @returns {string}
32
+ * Generates SQL statement that produces the average of a given expression
33
+ * @param {string} x - The expression to average
34
+ * @returns {string} - SQL statement
51
35
  */
52
- contains: (...args) => `(ifnull(instr(${args}),0) > 0)`,
36
+ average: x => `avg(${x})`,
37
+
53
38
  /**
54
39
  * Generates SQL statement that produces the number of elements in a given collection
55
- * @param {string} x
56
- * @returns {string}
40
+ * @param {string} x - The collection input
41
+ * @returns {string} - SQL statement
57
42
  */
58
- count: x => `count(${x || '*'})`,
43
+ count: x => `count(${x?.val || x || '*'})`,
44
+
59
45
  /**
60
46
  * Generates SQL statement that produces the number of distinct values of a given expression
61
- * @param {string} x
62
- * @returns {string}
63
- */
64
- countdistinct: x => `count(distinct ${x || cds.error`countdistinct requires a ref to be counted`})`,
65
- /**
66
- * Generates SQL statement that produces the index of the first occurrence of the second string in the first string
67
- * @param {string} x
68
- * @param {string} y
69
- * @returns {string}
47
+ * @param {string} x - The expression input
48
+ * @returns {string} - SQL statement
70
49
  */
71
- indexof: (x, y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed
50
+ countdistinct: x => `count(distinct ${x.val || x || '*'})`,
51
+
52
+ // ==============================
53
+ // String Functions
54
+ // ==============================
55
+
72
56
  /**
73
- * Generates SQL statement that produces a boolean value indicating whether the first string starts with the second string
74
- * @param {string} x
75
- * @param {string} y
76
- * @returns {string}
57
+ * Generates SQL statement that produces the length of a given string
58
+ * @param {string} x - The string input
59
+ * @returns {string} - SQL statement
77
60
  */
78
- startswith: (x, y) => `coalesce(instr(${x},${y}) = 1,false)`, // sqlite instr is 1 indexed
79
- // takes the end of the string of the size of the target and compares it with the target
61
+ length: x => `length(${x})`,
62
+
80
63
  /**
81
- * Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string
82
- * @param {string} x
83
- * @param {string} y
84
- * @returns {string}
64
+ * Generates SQL statement that produces a string with all provided strings concatenated
65
+ * @param {...string} args - The strings to concatenate
66
+ * @returns {string} - SQL statement
85
67
  */
86
- endswith: (x, y) => `coalesce(substr(${x}, length(${x}) + 1 - length(${y})) = ${y},false)`,
68
+ concat: (...args) => args.map(a => (a.xpr ? `(${a})` : a)).join(' || '),
69
+
87
70
  /**
88
71
  * Generates SQL statement that produces the substring of a given string
89
72
  * @example
@@ -92,266 +75,115 @@ const StandardFunctions = {
92
75
  * @example
93
76
  * // returns 'b'
94
77
  * {func:'substring',args:[{val:'abc'},{val:1},{val:1}]}
95
- * @param {string} x
96
- * @param {string} y
97
- * @param {string} z
98
- * @returns {string}
78
+ * @param {string} x - The string input
79
+ * @param {string} y - The starting position
80
+ * @param {string} [z] - Optional length of the substring
81
+ * @returns {string} - SQL statement
99
82
  */
100
83
  substring: (x, y, z) =>
101
84
  z
102
- ? `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z} )`
103
- : `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end )`,
85
+ ? `substr(${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z})`
86
+ : `substr(${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end)`,
104
87
 
105
- // String Functions
106
- /**
107
- * Generates SQL statement that matches the given string against a regular expression
108
- * @param {string} x
109
- * @param {string} y
110
- * @returns {string}
111
- */
112
- matchesPattern: (x, y) => `(${x} regexp ${y})`,
113
- /**
114
- * Generates SQL statement that matches the given string against a regular expression
115
- * @param {string} x
116
- * @param {string} y
117
- * @returns {string}
118
- */
119
- matchespattern: (x, y) => `(${x} regexp ${y})`,
120
88
  /**
121
89
  * Generates SQL statement that produces the lower case value of a given string
122
- * @param {string} x
123
- * @returns {string}
90
+ * @param {string} x - The string input
91
+ * @returns {string} - SQL statement
124
92
  */
125
93
  tolower: x => `lower(${x})`,
94
+
126
95
  /**
127
96
  * Generates SQL statement that produces the upper case value of a given string
128
- * @param {string} x
129
- * @returns {string}
97
+ * @param {string} x - The string input
98
+ * @returns {string} - SQL statement
130
99
  */
131
100
  toupper: x => `upper(${x})`,
101
+
132
102
  /**
133
103
  * Generates SQL statement that produces the trimmed value of a given string
134
- * @param {string} x
135
- * @returns {string}
104
+ * @param {string} x - The string input
105
+ * @returns {string} - SQL statement
136
106
  */
137
107
  trim: x => `trim(${x})`,
138
108
 
109
+ // ==============================
139
110
  // Arithmetic Functions
111
+ // ==============================
112
+
140
113
  /**
141
114
  * Generates SQL statement that produces the rounded up value of a given number
142
- * @param {string} x
143
- * @returns {string}
115
+ * @param {string} x - The number input
116
+ * @returns {string} - SQL statement
144
117
  */
145
118
  ceiling: x => `ceil(${x})`,
119
+
146
120
  /**
147
121
  * Generates SQL statement that produces the rounded down value of a given number
148
- * @param {string} x
149
- * @returns {string}
122
+ * @param {string} x - The number input
123
+ * @returns {string} - SQL statement
150
124
  */
151
125
  floor: x => `floor(${x})`,
126
+
152
127
  /**
153
128
  * Generates SQL statement that produces the rounded value of a given number
154
- * @param {string} x
155
- * @param {string} p precision
156
- * @returns {string}
129
+ * @param {string} x - The number input
130
+ * @param {string} p - The precision
131
+ * @returns {string} - SQL statement
157
132
  */
158
133
  round: (x, p) => `round(${x}${p ? `,${p}` : ''})`,
159
134
 
135
+ // ==============================
160
136
  // Date and Time Functions
137
+ // ==============================
161
138
 
162
139
  /**
163
140
  * Generates SQL statement that produces current point in time (date and time with time zone)
164
- * @returns {string}
141
+ * @returns {string} - SQL statement
165
142
  */
166
143
  now: function () {
167
144
  return this.session_context({ val: '$now' })
168
145
  },
169
- /**
170
- * Generates SQL statement that produces the year of a given timestamp
171
- * @param {string} x
172
- * @returns {string}
173
- * /
174
- year: x => `cast( strftime('%Y',${x}) as Integer )`,
175
- /**
176
- * Generates SQL statement that produces the month of a given timestamp
177
- * @param {string} x
178
- * @returns {string}
179
- * /
180
- month: x => `cast( strftime('%m',${x}) as Integer )`,
181
- /**
182
- * Generates SQL statement that produces the day of a given timestamp
183
- * @param {string} x
184
- * @returns {string}
185
- * /
186
- day: x => `cast( strftime('%d',${x}) as Integer )`,
187
- /**
188
- * Generates SQL statement that produces the hours of a given timestamp
189
- * @param {string} x
190
- * @returns {string}
191
- * /
192
- hour: x => `cast( strftime('%H',${x}) as Integer )`,
193
- /**
194
- * Generates SQL statement that produces the minutes of a given timestamp
195
- * @param {string} x
196
- * @returns {string}
197
- * /
198
- minute: x => `cast( strftime('%M',${x}) as Integer )`,
199
- /**
200
- * Generates SQL statement that produces the seconds of a given timestamp
201
- * @param {string} x
202
- * @returns {string}
203
- * /
204
- second: x => `cast( strftime('%S',${x}) as Integer )`,
205
146
 
206
- // REVISIT: make precision configurable
207
147
  /**
208
- * Generates SQL statement that produces the fractional seconds of a given timestamp
209
- * @param {string} x
210
- * @returns {string}
148
+ * Maximum date time value
149
+ * @returns {string} - SQL statement
211
150
  */
212
- fractionalseconds: x => `cast( substr( strftime('%f', ${x}), length(strftime('%f', ${x})) - 3) as REAL)`,
151
+ maxdatetime: () => `'9999-12-31T23:59:59.999Z'`,
213
152
 
214
153
  /**
215
- * maximum date time value
216
- * @returns {string}
217
- */
218
- maxdatetime: () => "'9999-12-31T23:59:59.999Z'",
219
- /**
220
- * minimum date time value
221
- * @returns {string}
154
+ * Minimum date time value
155
+ * @returns {string} - SQL statement
222
156
  */
223
- mindatetime: () => "'0001-01-01T00:00:00.000Z'",
224
-
225
- // odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
226
- // P -> duration indicator
227
- // D -> days, T -> Time seperator, H -> hours, M -> minutes, S -> fractional seconds
228
- // By splitting the DT and calculating the seconds of the time separate from the day
229
- // it possible to determine the full amount of seconds by adding them together as fractionals and multiplying
230
- // the number of seconds in a day
231
- // As sqlite is most accurate with juliandays it is better to do then then using actual second function
232
- // while the odata specification states that the seconds has to be fractional which only julianday allows
233
- /**
234
- * Generates SQL statement that produces an OData compliant duration string like: P12DT23H59M59.999999999999S
235
- * @param {string} x
236
- * @returns {string}
237
- */
238
- totalseconds: x => `(
239
- (
240
- (
241
- cast(substr(${x},2,instr(${x},'DT') - 2) as Integer)
242
- ) + (
243
- julianday(
244
- '-4713-11-25T' ||
245
- replace(
246
- replace(
247
- replace(
248
- substr(${x},instr(${x},'DT') + 2),
249
- 'H',':'
250
- ),'M',':'
251
- ),'S','Z'
252
- )
253
- ) - 0.5
254
- )
255
- ) * 86400
256
- )`
157
+ mindatetime: () => `'0001-01-01T00:00:00.000Z'`,
257
158
  }
258
159
 
259
160
  const HANAFunctions = {
260
- // https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
261
-
262
161
  /**
263
162
  * Generates SQL statement that calls the session_context function with the given parameter
264
- * @param {string} x session variable name or SQL expression
265
- * @returns {string}
163
+ * @param {string} x - The session variable name or SQL expression
164
+ * @returns {string} - SQL statement
266
165
  */
267
- session_context: x => `session_context('${x.val}')`,
166
+ session_context: x => `session_context('${x.val}')`,
268
167
 
269
- // Time functions
270
- current_date: p => (p ? `current_date(${p})` : 'current_date'),
271
- current_time: p => (p ? `current_time(${p})` : 'current_time'),
272
- current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
273
- /**
274
- * Generates SQL statement that calculates the difference in 100nanoseconds between two timestamps
275
- * @param {string} x left timestamp
276
- * @param {string} y right timestamp
277
- * @returns {string}
278
- */
279
- nano100_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 864000000000`,
280
168
  /**
281
- * Generates SQL statement that calculates the difference in seconds between two timestamps
282
- * @param {string} x left timestamp
283
- * @param {string} y right timestamp
284
- * @returns {string}
169
+ * Generates SQL statement for the current date
170
+ * @returns {string} - SQL statement
285
171
  */
286
- seconds_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 86400`,
287
- // Calculates the difference in full days using julian day
288
- // Using the exact time of the day to determine whether 24 hours have passed or not to add the final day
289
- // When just comparing the julianday values with each other there are leap seconds included
290
- // Which on the day resolution are included as the individual days therefor ignoring them to match HANA
291
- /**
292
- * Generates SQL statement that calculates the difference in days between two timestamps
293
- * @param {string} x left timestamp
294
- * @param {string} y right timestamp
295
- * @returns {string}
296
- */
297
- days_between: (x, y) => `(
298
- cast ( julianday(${y}) as Integer ) - cast ( julianday(${x}) as Integer )
299
- ) + (
300
- case
301
- when ( julianday(${y}) < julianday(${x}) ) then
302
- (cast( strftime('%H%M%S%f0000', ${y}) as Integer ) < cast( strftime('%H%M%S%f0000', ${x}) as Integer ))
303
- else
304
- (cast( strftime('%H%M%S%f0000', ${y}) as Integer ) > cast( strftime('%H%M%S%f0000', ${x}) as Integer )) * -1
305
- end
306
- )`,
172
+ current_date: () => 'current_date',
307
173
 
308
- // (y1 - y0) * 12 + (m1 - m0) + (t1 < t0) * -1
309
- /* '%d%H%M%S%f' returns as a number like which results in an equal check to:
310
- (
311
- d1 < d0 ||
312
- (d1 = d0 && h1 < h0) ||
313
- (d1 = d0 && h1 = h0 && m1 < m0) ||
314
- (d1 = d0 && h1 = h0 && m1 = m0 && s1 < s0) ||
315
- (d1 = d0 && h1 = h0 && m1 = m0 && s1 = s0 && ms1 < ms0)
316
- )
317
- Which will remove the current month if the time of the month is below the time of the month of the start date
318
- It should not matter that the number of days in the month is different as for a month to have passed
319
- the time of the month would have to be higher then the time of the month of the start date
320
-
321
- Also check whether the result will be positive or negative to make sure to not subtract an extra month
322
- */
323
174
  /**
324
- * Generates SQL statement that calculates the difference in months between two timestamps
325
- * @param {string} x left timestamp
326
- * @param {string} y right timestamp
327
- * @returns {string}
175
+ * Generates SQL statement for the current time
176
+ * @param {string} [p] - Optional precision parameter
177
+ * @returns {string} - SQL statement
328
178
  */
329
- months_between: (x, y) => `
330
- (
331
- (
332
- ( cast( strftime('%Y', ${y}) as Integer ) - cast( strftime('%Y', ${x}) as Integer ) ) * 12
333
- ) + (
334
- cast( strftime('%m', ${y}) as Integer ) - cast( strftime('%m', ${x}) as Integer )
335
- ) + (
336
- (
337
- case
338
- when ( cast( strftime('%Y%m', ${y}) as Integer ) < cast( strftime('%Y%m', ${x}) as Integer ) ) then
339
- (cast( strftime('%d%H%M%S%f0000', ${y}) as Integer ) > cast( strftime('%d%H%M%S%f0000', ${x}) as Integer ))
340
- else
341
- (cast( strftime('%d%H%M%S%f0000', ${y}) as Integer ) < cast( strftime('%d%H%M%S%f0000', ${x}) as Integer )) * -1
342
- end
343
- )
344
- )
345
- )`,
179
+ current_time: p => (p ? `current_time(${p})` : 'current_time'),
180
+
346
181
  /**
347
- * Generates SQL statement that calculates the difference in years between two timestamps
348
- * @param {string} x left timestamp
349
- * @param {string} y right timestamp
350
- * @returns {string}
182
+ * Generates SQL statement for the current timestamp
183
+ * @param {string} [p] - Optional precision parameter
184
+ * @returns {string} - SQL statement
351
185
  */
352
- years_between(x, y) {
353
- return `floor(${this.months_between(x, y)} / 12)`
354
- },
186
+ current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
355
187
  }
356
188
 
357
189
  for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
package/lib/cqn2sql.js CHANGED
@@ -92,7 +92,7 @@ class CQN2SQLRenderer {
92
92
  if (values && !Array.isArray(values)) {
93
93
  values = [values]
94
94
  }
95
- DEBUG(this.sql, ...values)
95
+ DEBUG(this.sql, values)
96
96
  }
97
97
 
98
98
 
@@ -263,7 +263,13 @@ class CQN2SQLRenderer {
263
263
  * @returns {string} SQL
264
264
  */
265
265
  SELECT_columns(q) {
266
- return (q.SELECT.columns ?? ['*']).map(x => this.column_expr(x, q))
266
+ const ret = []
267
+ const arr = q.SELECT.columns ?? ['*']
268
+ for (const x of arr) {
269
+ if (x.SELECT?.count) arr.push(this.SELECT_count(x))
270
+ ret.push(this.column_expr(x, q))
271
+ }
272
+ return ret
267
273
  }
268
274
 
269
275
  /**
@@ -292,24 +298,12 @@ class CQN2SQLRenderer {
292
298
  ? x => {
293
299
  const name = this.column_name(x)
294
300
  const escaped = `${name.replace(/"/g, '""')}`
295
- let col = `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
296
- if (x.SELECT?.count) {
297
- // Return both the sub select and the count for @odata.count
298
- const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
299
- return [col, `${this.expr(qc)} AS "${escaped}@odata.count"`]
300
- }
301
- return col
301
+ return `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
302
302
  }
303
303
  : x => {
304
304
  const name = this.column_name(x)
305
305
  const escaped = `${name.replace(/"/g, '""')}`
306
- let col = `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
307
- if (x.SELECT?.count) {
308
- // Return both the sub select and the count for @odata.count
309
- const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
310
- return [col, `'$."${escaped}@odata.count"',${this.expr(qc)}`]
311
- }
312
- return col
306
+ return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
313
307
  }).flat()
314
308
 
315
309
  if (isSimple) return `SELECT ${cols} FROM (${sql})`
@@ -322,6 +316,17 @@ class CQN2SQLRenderer {
322
316
  return `SELECT ${isRoot || SELECT.one ? obj.replace('jsonb', 'json') : `jsonb_group_array(${obj})`} as _json_ FROM (${sql})`
323
317
  }
324
318
 
319
+ SELECT_count(q) {
320
+ const countQuery = cds.ql.clone(q, {
321
+ columns: [{ func: 'count' }],
322
+ one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
323
+ })
324
+ countQuery.as = q.as + '@odata.count'
325
+ countQuery.elements = undefined
326
+ countQuery.element = cds.builtin.types.Int64
327
+ return countQuery
328
+ }
329
+
325
330
  /**
326
331
  * Renders a SELECT column expression into generic SQL
327
332
  * @param {import('./infer/cqn').col} x
package/lib/cqn4sql.js CHANGED
@@ -60,6 +60,22 @@ function cqn4sql(originalQuery, model) {
60
60
  else if (having) inferred.SELECT.having = having
61
61
  }
62
62
  }
63
+ // query modifiers can also be defined in from ref leaf infix filter
64
+ // > SELECT from bookshop.Books[order by price] {ID}
65
+ if(inferred.SELECT?.from.ref) {
66
+ for(const [key, val] of Object.entries(inferred.SELECT.from.ref.at(-1))) {
67
+ if(key in { orderBy: 1, groupBy: 1 }) {
68
+ if(inferred.SELECT[key]) inferred.SELECT[key].push(...val)
69
+ else inferred.SELECT[key] = val
70
+ } else if(key === 'limit') {
71
+ // limit defined on the query has precedence
72
+ if(!inferred.SELECT.limit) inferred.SELECT.limit = val
73
+ } else if(key === 'having') {
74
+ if(!inferred.SELECT.having) inferred.SELECT.having = val
75
+ else inferred.SELECT.having.push('and', ...val)
76
+ }
77
+ }
78
+ }
63
79
  inferred = infer(inferred, model)
64
80
  // if the query has custom joins we don't want to transform it
65
81
  // TODO: move all the way to the top of this function once cds.infer supports joins as well
@@ -807,11 +823,11 @@ function cqn4sql(originalQuery, model) {
807
823
  // `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
808
824
  const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
809
825
  const subqueryBase = {}
810
- for (const [key, value] of Object.entries(column)) {
811
- if (!(key in { ref: true, expand: true })) {
812
- subqueryBase[key] = value
813
- }
826
+ const queryModifiers = { ...column }
827
+ for (const [key, value] of Object.entries(queryModifiers)) {
828
+ if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 }) subqueryBase[key] = value
814
829
  }
830
+
815
831
  const subquery = {
816
832
  SELECT: {
817
833
  ...subqueryBase,
@@ -877,8 +893,8 @@ function cqn4sql(originalQuery, model) {
877
893
 
878
894
  // to be attached to dummy query
879
895
  const elements = {}
880
- const wildcardIndex = column.expand.findIndex(e => e === '*')
881
- if (wildcardIndex !== -1) {
896
+ const containsWildcard = column.expand.includes('*')
897
+ if (containsWildcard) {
882
898
  // expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
883
899
  return null
884
900
  }
@@ -888,8 +904,10 @@ function cqn4sql(originalQuery, model) {
888
904
 
889
905
  if (expand.expand) {
890
906
  const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
891
- setElementOnColumns(nested, expand.element)
892
- elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
907
+ if(nested) {
908
+ setElementOnColumns(nested, expand.element)
909
+ elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
910
+ }
893
911
  return nested
894
912
  }
895
913
 
@@ -910,7 +928,11 @@ function cqn4sql(originalQuery, model) {
910
928
  elements[c.as || c.ref.at(-1)] = c.element
911
929
  })
912
930
  return res
913
- })
931
+ }).filter(c => c)
932
+
933
+ if (expandedColumns.length === 0) {
934
+ return null
935
+ }
914
936
 
915
937
  const SELECT = {
916
938
  from: null,
@@ -1225,8 +1247,7 @@ function cqn4sql(originalQuery, model) {
1225
1247
  if (flattenThisForeignKey) {
1226
1248
  const fkElement = getElementForRef(k.ref, getDefinition(element.target))
1227
1249
  let fkBaseName
1228
- if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
1229
- fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
1250
+ if (!leafAssoc || leafAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
1230
1251
  // e.g. if foreign key is accessed via infix filter - use join alias to access key in target
1231
1252
  else fkBaseName = k.ref.at(-1)
1232
1253
  const fkPath = [...csnPath, k.ref.at(-1)]
@@ -1478,8 +1499,7 @@ function cqn4sql(originalQuery, model) {
1478
1499
  // reject associations in expression, except if we are in an infix filter -> $baseLink is set
1479
1500
  assertNoStructInXpr(token, $baseLink)
1480
1501
  // reject virtual elements in expressions as they will lead to a sql error down the line
1481
- if(definition?.virtual)
1482
- throw new Error(`Virtual elements are not allowed in expressions`)
1502
+ if (definition?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
1483
1503
 
1484
1504
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1485
1505
  if (token.ref) {
@@ -116,9 +116,11 @@ function infer(originalQuery, model) {
116
116
 
117
117
  inferArg(from, null, null, { inFrom: true })
118
118
  const alias =
119
- from.uniqueSubqueryAlias ||
120
- from.as ||
121
- (ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1])
119
+ from.uniqueSubqueryAlias ||
120
+ from.as ||
121
+ (ref.length === 1
122
+ ? first.substring(first.lastIndexOf('.') + 1)
123
+ : (ref.at(-1).id || ref.at(-1)));
122
124
  if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
123
125
  querySources[alias] = { definition: target, args }
124
126
  const last = from.$refLinks.at(-1)
@@ -134,7 +136,7 @@ function infer(originalQuery, model) {
134
136
  } else if (typeof from === 'string') {
135
137
  // TODO: Create unique alias, what about duplicates?
136
138
  const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
137
- querySources[/([^.]*)$/.exec(from)[0]] = { definition }
139
+ querySources[from.substring(from.lastIndexOf('.') + 1)] = { definition }
138
140
  } else if (from.SET) {
139
141
  infer(from, model)
140
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.17.1",
3
+ "version": "1.18.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": {