@cap-js/db-service 1.17.2 → 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 +15 -0
- package/lib/cql-functions.js +88 -256
- package/lib/cqn2sql.js +21 -16
- package/lib/cqn4sql.js +33 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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
|
+
|
|
7
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)
|
|
8
23
|
|
|
9
24
|
|
package/lib/cql-functions.js
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
|
|
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(
|
|
27
|
-
//
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
49
|
-
* @param
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
countdistinct: x => `count(distinct ${x.val || x || '*'})`,
|
|
51
|
+
|
|
52
|
+
// ==============================
|
|
53
|
+
// String Functions
|
|
54
|
+
// ==============================
|
|
55
|
+
|
|
72
56
|
/**
|
|
73
|
-
* Generates SQL statement that produces
|
|
74
|
-
* @param {string} x
|
|
75
|
-
* @
|
|
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
|
-
|
|
79
|
-
|
|
61
|
+
length: x => `length(${x})`,
|
|
62
|
+
|
|
80
63
|
/**
|
|
81
|
-
* Generates SQL statement that produces a
|
|
82
|
-
* @param
|
|
83
|
-
* @
|
|
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
|
-
|
|
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(
|
|
103
|
-
: `substr(
|
|
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
|
-
*
|
|
209
|
-
* @
|
|
210
|
-
* @returns {string}
|
|
148
|
+
* Maximum date time value
|
|
149
|
+
* @returns {string} - SQL statement
|
|
211
150
|
*/
|
|
212
|
-
|
|
151
|
+
maxdatetime: () => `'9999-12-31T23:59:59.999Z'`,
|
|
213
152
|
|
|
214
153
|
/**
|
|
215
|
-
*
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
282
|
-
* @
|
|
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
|
-
|
|
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
|
|
325
|
-
* @param {string}
|
|
326
|
-
* @
|
|
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
|
-
|
|
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
|
|
348
|
-
* @param {string}
|
|
349
|
-
* @
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
|
881
|
-
if (
|
|
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
|
-
|
|
892
|
-
|
|
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) {
|
package/package.json
CHANGED