@sap/cds-dk 10.0.3 → 10.0.5
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 +28 -0
- package/bin/debug.js +6 -2
- package/bin/up.js +1 -1
- package/bin/watch.js +1 -0
- package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
- package/lib/build/plugins/nodejs/index.js +6 -3
- package/lib/build/util.js +1 -1
- package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
- package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
- package/lib/init/template/data/as-csv.js +4 -2
- package/lib/init/template/data/as-json.js +4 -1
- package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
- package/lib/init/template/ias/files/values.yaml.hbs +4 -4
- package/lib/init/template/portal/files/values.yaml.hbs +12 -0
- package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
- package/lib/init/template/typer/files/xsconfig.json +1 -1
- package/lib/init/template/typescript/files/package.json +1 -1
- package/lib/lint/eslintHelp.json +1 -1
- package/lib/watch/esm-resolve-registration.js +27 -0
- package/lib/watch/vite-watch.js +1 -1
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
- package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
- package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
- package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
- package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
- package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
- package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
- package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
- package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
- package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
- package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
- package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
- package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
- package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
- package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
- package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
- package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
- package/node_modules/@sap/cds-compiler/package.json +1 -1
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- package/package.json +5 -3
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
|
|
5
|
+
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
6
|
+
const StandardFunctions = {
|
|
7
|
+
/**
|
|
8
|
+
* Generates SQL statement that produces a runtime compatible error object
|
|
9
|
+
* @param {string|object} message - The i18n key or message of the error object
|
|
10
|
+
* @param {Array<xpr>} args - The arguments to apply to the i18n string
|
|
11
|
+
* @param {Array<xpr>} targets - The name of the element that the error is related to
|
|
12
|
+
* @return {string} - SQL statement
|
|
13
|
+
*/
|
|
14
|
+
error: function (message, args, targets) {
|
|
15
|
+
targets = targets && (targets.list || (targets.val || targets.ref) && [targets])
|
|
16
|
+
if (Array.isArray(targets)) targets = targets.map(e => e.ref && { val: e.ref.at(-1) } || e)
|
|
17
|
+
args = args && (args.list || (args.val || args.ref) && [args])
|
|
18
|
+
|
|
19
|
+
return `(${this.SELECT({
|
|
20
|
+
SELECT: {
|
|
21
|
+
expand: 'root',
|
|
22
|
+
columns: [
|
|
23
|
+
{
|
|
24
|
+
__proto__: (message || { val: null }),
|
|
25
|
+
as: 'message',
|
|
26
|
+
},
|
|
27
|
+
args ? {
|
|
28
|
+
func: 'json_array',
|
|
29
|
+
args: args,
|
|
30
|
+
as: 'args',
|
|
31
|
+
element: cds.builtin.types.Map,
|
|
32
|
+
} : { val: null, as: 'args' },
|
|
33
|
+
targets ? {
|
|
34
|
+
func: 'json_array',
|
|
35
|
+
args: targets,
|
|
36
|
+
as: 'targets',
|
|
37
|
+
element: cds.builtin.types.Map,
|
|
38
|
+
} : { val: null, as: 'targets' },
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
elements: {
|
|
42
|
+
message: cds.builtin.types.String,
|
|
43
|
+
args: cds.builtin.types.Map,
|
|
44
|
+
targets: cds.builtin.types.Map,
|
|
45
|
+
}
|
|
46
|
+
})})`
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns
|
|
51
|
+
* @param {string} ref - The reference object containing column information
|
|
52
|
+
* @param {string} arg - The argument object containing the search value
|
|
53
|
+
* @returns {string} - SQL statement
|
|
54
|
+
*/
|
|
55
|
+
search: function (ref, arg) {
|
|
56
|
+
if (!('val' in arg)) throw new Error('Only single value arguments are allowed for $search')
|
|
57
|
+
// Only apply first search term, rest is ignored
|
|
58
|
+
const sub = /("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/.exec(arg.val)
|
|
59
|
+
let val
|
|
60
|
+
try {
|
|
61
|
+
val = (sub[2] ? JSON.parse(sub[2]) : sub[3]) || ''
|
|
62
|
+
} catch {
|
|
63
|
+
val = sub[2] || sub[3] || ''
|
|
64
|
+
}
|
|
65
|
+
arg.val = val
|
|
66
|
+
const refs = ref.list || [ref]
|
|
67
|
+
return `(${refs.map(ref => this.expr({
|
|
68
|
+
func: 'contains',
|
|
69
|
+
args: [
|
|
70
|
+
{ func: 'tolower', args: [ref] },
|
|
71
|
+
{ func: 'tolower', args: [arg] },
|
|
72
|
+
]
|
|
73
|
+
})).join(' or ')})`
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ==============================
|
|
77
|
+
// Aggregation Functions
|
|
78
|
+
// ==============================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generates SQL statement that produces the average of a given expression
|
|
82
|
+
* @param {string} x - The expression to average
|
|
83
|
+
* @returns {string} - SQL statement
|
|
84
|
+
*/
|
|
85
|
+
average: x => `avg(${x})`,
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generates SQL statement that produces the number of elements in a given collection
|
|
89
|
+
* @param {string} x - The collection input
|
|
90
|
+
* @returns {string} - SQL statement
|
|
91
|
+
*/
|
|
92
|
+
count: x => `count(${x?.val || x || '*'})`,
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generates SQL statement that produces the number of distinct values of a given expression
|
|
96
|
+
* @param {string} x - The expression input
|
|
97
|
+
* @returns {string} - SQL statement
|
|
98
|
+
*/
|
|
99
|
+
countdistinct: x => `count(distinct ${x.val || x || '*'})`,
|
|
100
|
+
|
|
101
|
+
// ==============================
|
|
102
|
+
// String Functions
|
|
103
|
+
// ==============================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates SQL statement that produces the length of a given string
|
|
107
|
+
* @param {string} x - The string input
|
|
108
|
+
* @returns {string} - SQL statement
|
|
109
|
+
*/
|
|
110
|
+
length: x => `length(${x})`,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generates SQL statement that produces a string with all provided strings concatenated
|
|
114
|
+
* @param {...string} args - The strings to concatenate
|
|
115
|
+
* @returns {string} - SQL statement
|
|
116
|
+
*/
|
|
117
|
+
concat: (...args) => args.map(a => (a.xpr ? `(${a})` : a)).join(' || '),
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generates SQL statement that produces the substring of a given string
|
|
121
|
+
* @example
|
|
122
|
+
* // returns 'bc'
|
|
123
|
+
* {func:'substring',args:[{val:'abc'},{val:1}]}
|
|
124
|
+
* @example
|
|
125
|
+
* // returns 'b'
|
|
126
|
+
* {func:'substring',args:[{val:'abc'},{val:1},{val:1}]}
|
|
127
|
+
* @param {string} x - The string input
|
|
128
|
+
* @param {string} y - The starting position
|
|
129
|
+
* @param {string} [z] - Optional length of the substring
|
|
130
|
+
* @returns {string} - SQL statement
|
|
131
|
+
*/
|
|
132
|
+
substring: (x, y, z) =>
|
|
133
|
+
z
|
|
134
|
+
? `substr(${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z})`
|
|
135
|
+
: `substr(${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end)`,
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generates SQL statement that produces the lower case value of a given string
|
|
139
|
+
* @param {string} x - The string input
|
|
140
|
+
* @returns {string} - SQL statement
|
|
141
|
+
*/
|
|
142
|
+
tolower: x => `lower(${x})`,
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generates SQL statement that produces the upper case value of a given string
|
|
146
|
+
* @param {string} x - The string input
|
|
147
|
+
* @returns {string} - SQL statement
|
|
148
|
+
*/
|
|
149
|
+
toupper: x => `upper(${x})`,
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generates SQL statement that produces the trimmed value of a given string
|
|
153
|
+
* @param {string} x - The string input
|
|
154
|
+
* @returns {string} - SQL statement
|
|
155
|
+
*/
|
|
156
|
+
trim: x => `trim(${x})`,
|
|
157
|
+
|
|
158
|
+
// ==============================
|
|
159
|
+
// Arithmetic Functions
|
|
160
|
+
// ==============================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generates SQL statement that produces the rounded up value of a given number
|
|
164
|
+
* @param {string} x - The number input
|
|
165
|
+
* @returns {string} - SQL statement
|
|
166
|
+
*/
|
|
167
|
+
ceiling: x => `ceil(${x})`,
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generates SQL statement that produces the rounded down value of a given number
|
|
171
|
+
* @param {string} x - The number input
|
|
172
|
+
* @returns {string} - SQL statement
|
|
173
|
+
*/
|
|
174
|
+
floor: x => `floor(${x})`,
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generates SQL statement that produces the rounded value of a given number
|
|
178
|
+
* @param {string} x - The number input
|
|
179
|
+
* @param {string} p - The precision
|
|
180
|
+
* @returns {string} - SQL statement
|
|
181
|
+
*/
|
|
182
|
+
round: (x, p) => `round(${x}${p ? `,${p}` : ''})`,
|
|
183
|
+
|
|
184
|
+
// ==============================
|
|
185
|
+
// Date and Time Functions
|
|
186
|
+
// ==============================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generates SQL statement that produces current point in time (date and time with time zone)
|
|
190
|
+
* @returns {string} - SQL statement
|
|
191
|
+
*/
|
|
192
|
+
now: function () {
|
|
193
|
+
return this.expr({ func: 'session_context', args: [{ val: '$now' }] })
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Maximum date time value
|
|
198
|
+
* @returns {string} - SQL statement
|
|
199
|
+
*/
|
|
200
|
+
maxdatetime: () => `'9999-12-31T23:59:59.999Z'`,
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Minimum date time value
|
|
204
|
+
* @returns {string} - SQL statement
|
|
205
|
+
*/
|
|
206
|
+
mindatetime: () => `'0001-01-01T00:00:00.000Z'`,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const HANAFunctions = {
|
|
210
|
+
/**
|
|
211
|
+
* Generates SQL statement that calls the session_context function with the given parameter
|
|
212
|
+
* @param {string} x - The session variable name or SQL expression
|
|
213
|
+
* @returns {string} - SQL statement
|
|
214
|
+
*/
|
|
215
|
+
session_context: x => `session_context('${x.val}')`,
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Generates SQL statement for the current date
|
|
219
|
+
* @returns {string} - SQL statement
|
|
220
|
+
*/
|
|
221
|
+
current_date: () => 'current_date',
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generates SQL statement for the current time
|
|
225
|
+
* @param {string} [p] - Optional precision parameter
|
|
226
|
+
* @returns {string} - SQL statement
|
|
227
|
+
*/
|
|
228
|
+
current_time: p => (p ? `current_time(${p})` : 'current_time'),
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generates SQL statement for the current timestamp
|
|
232
|
+
* @param {string} [p] - Optional precision parameter
|
|
233
|
+
* @returns {string} - SQL statement
|
|
234
|
+
*/
|
|
235
|
+
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generates SQL statement for the hierarchy function
|
|
239
|
+
* @param {string} [p] -
|
|
240
|
+
* @returns {string} - SQL statement
|
|
241
|
+
*/
|
|
242
|
+
HIERARCHY: function (args) {
|
|
243
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
244
|
+
let src = args.xpr[1]
|
|
245
|
+
|
|
246
|
+
// Ensure that the orderBy column are exposed by the source for hierarchy sorting
|
|
247
|
+
let orderBy = args.xpr.find((_, i, arr) => /ORDER/i.test(arr[i - 2]) && /BY/i.test(arr[i - 1]))
|
|
248
|
+
// use ![] instead of ""
|
|
249
|
+
if (orderBy && typeof orderBy === 'string') {
|
|
250
|
+
orderBy = orderBy.replace(/"([^"]*)"/g, '![$1]');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const passThroughColumns = src.SELECT.columns.map(c => ({ ref: ['Source', this.column_name(c)] }))
|
|
254
|
+
src.as = 'H' + (uniqueCounter++)
|
|
255
|
+
src = this.expr(this.with(src))
|
|
256
|
+
|
|
257
|
+
let recursive = cds.ql(`
|
|
258
|
+
SELECT
|
|
259
|
+
1 as HIERARCHY_LEVEL,
|
|
260
|
+
NODE_ID as HIERARCHY_ROOT_ID
|
|
261
|
+
FROM ${src} AS Source
|
|
262
|
+
WHERE parent_ID IS NULL
|
|
263
|
+
UNION ALL
|
|
264
|
+
SELECT
|
|
265
|
+
Parent.HIERARCHY_LEVEL + 1,
|
|
266
|
+
Parent.HIERARCHY_ROOT_ID
|
|
267
|
+
FROM ${src} AS Source
|
|
268
|
+
JOIN H${uniqueCounter} AS Parent ON Source.PARENT_ID=Parent.NODE_ID
|
|
269
|
+
ORDER BY HIERARCHY_LEVEL DESC${orderBy ? `,${orderBy}` : ''}`)
|
|
270
|
+
recursive.as = 'H' + (uniqueCounter++)
|
|
271
|
+
recursive.SET.args[0].SELECT.columns = [...recursive.SET.args[0].SELECT.columns, ...passThroughColumns]
|
|
272
|
+
recursive.SET.args[1].SELECT.columns = [...recursive.SET.args[1].SELECT.columns, ...passThroughColumns]
|
|
273
|
+
recursive = this.expr(this.with(recursive))
|
|
274
|
+
|
|
275
|
+
let ranked = cds.ql(`
|
|
276
|
+
SELECT
|
|
277
|
+
HIERARCHY_LEVEL,
|
|
278
|
+
row_number() over () as HIERARCHY_RANK,
|
|
279
|
+
HIERARCHY_ROOT_ID
|
|
280
|
+
FROM ${recursive} AS Source`)
|
|
281
|
+
ranked.as = 'H' + (uniqueCounter++)
|
|
282
|
+
ranked.SELECT.columns = [...ranked.SELECT.columns, ...passThroughColumns]
|
|
283
|
+
ranked = this.expr(this.with(ranked))
|
|
284
|
+
|
|
285
|
+
let Hierarchy = cds.ql(`
|
|
286
|
+
SELECT
|
|
287
|
+
HIERARCHY_LEVEL,
|
|
288
|
+
HIERARCHY_RANK,
|
|
289
|
+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.PARENT_ID) AS HIERARCHY_PARENT_RANK,
|
|
290
|
+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.HIERARCHY_ROOT_ID) AS HIERARCHY_ROOT_RANK,
|
|
291
|
+
coalesce(
|
|
292
|
+
(SELECT MIN(HIERARCHY_RANK) FROM ${ranked} AS Ranked WHERE Ranked.HIERARCHY_RANK > Source.HIERARCHY_RANK AND Ranked.HIERARCHY_LEVEL <= Source.HIERARCHY_LEVEL),
|
|
293
|
+
(SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
|
|
294
|
+
) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
|
|
295
|
+
FROM ${ranked} AS Source`)
|
|
296
|
+
Hierarchy.as = `H${uniqueCounter}`
|
|
297
|
+
Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
|
|
298
|
+
Hierarchy = this.expr(this.with(Hierarchy))
|
|
299
|
+
|
|
300
|
+
return Hierarchy
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generates SQL statement for the hierarchy_descendants function
|
|
305
|
+
* @param {string} [p] -
|
|
306
|
+
* @returns {string} - SQL statement
|
|
307
|
+
*/
|
|
308
|
+
HIERARCHY_DESCENDANTS: function (args) {
|
|
309
|
+
// Find Hierarchy function call source query
|
|
310
|
+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
|
|
311
|
+
// REVISIT: currently only supports func: HIERARCHY as source
|
|
312
|
+
const src = this.expr(args.xpr[1])
|
|
313
|
+
|
|
314
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
315
|
+
|
|
316
|
+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
|
|
317
|
+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
|
|
318
|
+
const distance = args.xpr.find((a, i, arr) => typeof a.val === 'number' && (/DISTANCE/i.test(arr[i - 1]) || /DISTANCE/i.test(arr[i - 2])))
|
|
319
|
+
const distanceFrom = args.xpr.find((a, i, arr) => /FROM/.test(a) && /DISTANCE/i.test(arr[i - 1]))
|
|
320
|
+
|
|
321
|
+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
|
|
322
|
+
|
|
323
|
+
let HierarchyDescendants = cds.ql(`
|
|
324
|
+
SELECT
|
|
325
|
+
HIERARCHY_LEVEL,
|
|
326
|
+
HIERARCHY_PARENT_RANK,
|
|
327
|
+
HIERARCHY_RANK,
|
|
328
|
+
HIERARCHY_ROOT_RANK,
|
|
329
|
+
HIERARCHY_TREE_SIZE,
|
|
330
|
+
0 as HIERARCHY_DISTANCE
|
|
331
|
+
FROM ${src} AS ![${alias}]
|
|
332
|
+
UNION ALL
|
|
333
|
+
SELECT
|
|
334
|
+
Source.HIERARCHY_LEVEL,
|
|
335
|
+
Source.HIERARCHY_PARENT_RANK,
|
|
336
|
+
Source.HIERARCHY_RANK,
|
|
337
|
+
Source.HIERARCHY_ROOT_RANK,
|
|
338
|
+
Source.HIERARCHY_TREE_SIZE,
|
|
339
|
+
Child.HIERARCHY_DISTANCE + 1
|
|
340
|
+
FROM ${src} AS Source
|
|
341
|
+
JOIN H${uniqueCounter} AS Child ON Source.PARENT_ID=Child.NODE_ID`)
|
|
342
|
+
HierarchyDescendants.as = 'H' + uniqueCounter
|
|
343
|
+
HierarchyDescendants.SET.args[0].SELECT.where = where.xpr
|
|
344
|
+
HierarchyDescendants.SET.args[0].SELECT.columns = [...HierarchyDescendants.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
|
|
345
|
+
HierarchyDescendants.SET.args[1].SELECT.columns = [...HierarchyDescendants.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
|
|
346
|
+
|
|
347
|
+
HierarchyDescendants = this.with(HierarchyDescendants)
|
|
348
|
+
HierarchyDescendants.as = 'HierarchyDescendants'
|
|
349
|
+
|
|
350
|
+
return this.expr({
|
|
351
|
+
SELECT: {
|
|
352
|
+
columns: [
|
|
353
|
+
{ ref: ['HIERARCHY_LEVEL'] },
|
|
354
|
+
{ ref: ['HIERARCHY_PARENT_RANK'] },
|
|
355
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
356
|
+
{ ref: ['HIERARCHY_ROOT_RANK'] },
|
|
357
|
+
{ ref: ['HIERARCHY_TREE_SIZE'] },
|
|
358
|
+
{
|
|
359
|
+
SELECT: {
|
|
360
|
+
columns: [{ func: 'MAX', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
|
|
361
|
+
from: HierarchyDescendants,
|
|
362
|
+
where: [{ ref: [HierarchyDescendants.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
|
|
363
|
+
},
|
|
364
|
+
as: 'HIERARCHY_DISTANCE',
|
|
365
|
+
},
|
|
366
|
+
...passThroughColumns,
|
|
367
|
+
],
|
|
368
|
+
from: { ref: [src] },
|
|
369
|
+
where: [
|
|
370
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
371
|
+
'IN',
|
|
372
|
+
{
|
|
373
|
+
SELECT: {
|
|
374
|
+
columns: [{ ref: ['HIERARCHY_RANK'] }],
|
|
375
|
+
from: HierarchyDescendants,
|
|
376
|
+
where: [{ ref: ['HIERARCHY_DISTANCE'] }, distanceFrom ? '>=' : '=', distance]
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generates SQL statement for the hierarchy_ancestors function
|
|
386
|
+
* @param {string} [p] -
|
|
387
|
+
* @returns {string} - SQL statement
|
|
388
|
+
*/
|
|
389
|
+
HIERARCHY_ANCESTORS: function (args) {
|
|
390
|
+
// Find Hierarchy function call source query
|
|
391
|
+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
|
|
392
|
+
// REVISIT: currently only supports func: HIERARCHY as source
|
|
393
|
+
const src = this.expr(args.xpr[1])
|
|
394
|
+
|
|
395
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
396
|
+
|
|
397
|
+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
|
|
398
|
+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
|
|
399
|
+
|
|
400
|
+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
|
|
401
|
+
|
|
402
|
+
let HierarchyAncestors = cds.ql(`
|
|
403
|
+
SELECT
|
|
404
|
+
HIERARCHY_LEVEL,
|
|
405
|
+
HIERARCHY_PARENT_RANK,
|
|
406
|
+
HIERARCHY_RANK,
|
|
407
|
+
HIERARCHY_ROOT_RANK,
|
|
408
|
+
HIERARCHY_TREE_SIZE,
|
|
409
|
+
0 as HIERARCHY_DISTANCE
|
|
410
|
+
FROM ${src} AS ![${alias}]
|
|
411
|
+
UNION ALL
|
|
412
|
+
SELECT
|
|
413
|
+
Source.HIERARCHY_LEVEL,
|
|
414
|
+
Source.HIERARCHY_PARENT_RANK,
|
|
415
|
+
Source.HIERARCHY_RANK,
|
|
416
|
+
Source.HIERARCHY_ROOT_RANK,
|
|
417
|
+
Source.HIERARCHY_TREE_SIZE,
|
|
418
|
+
Child.HIERARCHY_DISTANCE - 1
|
|
419
|
+
FROM ${src} AS Source
|
|
420
|
+
JOIN H${uniqueCounter} AS Child ON Source.NODE_ID=Child.PARENT_ID`)
|
|
421
|
+
HierarchyAncestors.as = 'H' + uniqueCounter
|
|
422
|
+
HierarchyAncestors.SET.args[0].SELECT.where = where.xpr
|
|
423
|
+
HierarchyAncestors.SET.args[0].SELECT.columns = [...HierarchyAncestors.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
|
|
424
|
+
HierarchyAncestors.SET.args[1].SELECT.columns = [...HierarchyAncestors.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
|
|
425
|
+
|
|
426
|
+
HierarchyAncestors = this.with(HierarchyAncestors)
|
|
427
|
+
HierarchyAncestors.as = 'HierarchyAncestors'
|
|
428
|
+
return this.expr({
|
|
429
|
+
SELECT: {
|
|
430
|
+
columns: [
|
|
431
|
+
{ ref: ['HIERARCHY_LEVEL'] },
|
|
432
|
+
{ ref: ['HIERARCHY_PARENT_RANK'] },
|
|
433
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
434
|
+
{ ref: ['HIERARCHY_ROOT_RANK'] },
|
|
435
|
+
{ ref: ['HIERARCHY_TREE_SIZE'] },
|
|
436
|
+
{
|
|
437
|
+
SELECT: {
|
|
438
|
+
columns: [{ func: 'MIN', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
|
|
439
|
+
from: HierarchyAncestors,
|
|
440
|
+
where: [{ ref: [HierarchyAncestors.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
|
|
441
|
+
},
|
|
442
|
+
as: 'HIERARCHY_DISTANCE',
|
|
443
|
+
},
|
|
444
|
+
...passThroughColumns,
|
|
445
|
+
],
|
|
446
|
+
from: { ref: [src] },
|
|
447
|
+
where: [
|
|
448
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
449
|
+
'IN',
|
|
450
|
+
{
|
|
451
|
+
SELECT: {
|
|
452
|
+
columns: [{ ref: ['HIERARCHY_RANK'] }],
|
|
453
|
+
from: HierarchyAncestors,
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
|
|
463
|
+
|
|
464
|
+
module.exports = { ...StandardFunctions, ...HANAFunctions }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
const CQN2SQL = require('./cqn2sql.js').class
|
|
4
|
+
|
|
5
|
+
class CQN2PQLRenderer extends CQN2SQL {
|
|
6
|
+
|
|
7
|
+
SELECT(q) {
|
|
8
|
+
this.values = undefined // inline all values
|
|
9
|
+
return (this.sql = super.SELECT(q)
|
|
10
|
+
.replaceAll('\n FROM', '\nFROM')
|
|
11
|
+
.replaceAll(/([^ ]) (FROM|WHERE|GROUP BY|HAVING|ORDER BY|LIMIT) /g, (a, b, c) => `${b}\n${c} `)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SELECT_columns(q) {
|
|
16
|
+
return super.SELECT_columns(q).map((c, i) => `${(i % 5 === 0) ? '\n ' : ' '}${c}${/ as /i.test(c) ? '\n' : ''}`).join(',')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
column_expr(x, q) {
|
|
20
|
+
// omit alias when target is a single source
|
|
21
|
+
if (q.SELECT.from.ref && x?.ref) x.ref = x.ref.slice(-1)
|
|
22
|
+
return super.column_expr(x, q)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SELECT_expand(q, sql) { return sql }
|
|
26
|
+
|
|
27
|
+
INSERT_entries(q) {
|
|
28
|
+
super.INSERT_entries(q)
|
|
29
|
+
this.sql = this.sql
|
|
30
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
31
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
32
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
33
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
34
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
35
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
36
|
+
.replaceAll('))', ')\n)')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
INSERT_rows(q) {
|
|
40
|
+
super.INSERT_rows(q)
|
|
41
|
+
this.sql = this.sql.replaceAll('SELECT', '\nSELECT')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
UPSERT(q) {
|
|
45
|
+
super.UPSERT(q)
|
|
46
|
+
this.sql = this.sql
|
|
47
|
+
.replaceAll('INSERT', 'UPSERT')
|
|
48
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
49
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
50
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
51
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
52
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
53
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
54
|
+
.replaceAll('))', ')\n)')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
expr(x) {
|
|
58
|
+
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
59
|
+
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
60
|
+
if (x.param) return wrap(this.param(x))
|
|
61
|
+
if ('ref' in x) return wrap(this.ref(x))
|
|
62
|
+
if ('val' in x) return wrap(this.val(x))
|
|
63
|
+
if ('func' in x) return wrap(this.func(x))
|
|
64
|
+
if ('xpr' in x) return wrap(this.xpr(x))
|
|
65
|
+
if ('list' in x) return wrap(this.list(x))
|
|
66
|
+
if ('SELECT' in x) return wrap(`(\n ${this.SELECT(x).replaceAll('\n', '\n ')}\n )`)
|
|
67
|
+
else throw cds.error`Unsupported expr: ${x}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
quote(s) { return s }
|
|
71
|
+
|
|
72
|
+
managed(columns, elements) {
|
|
73
|
+
const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation)
|
|
74
|
+
const keyZero = keys[0]
|
|
75
|
+
|
|
76
|
+
const ret = super.managed(columns, elements)
|
|
77
|
+
|
|
78
|
+
ret.forEach(c => {
|
|
79
|
+
const { name, insert, update, onInsert, onUpdate } = c
|
|
80
|
+
const element = elements?.[name]
|
|
81
|
+
c.upsert = keyZero && (
|
|
82
|
+
// upsert requires the keys to be provided for the existance join (default values optional)
|
|
83
|
+
element?.key
|
|
84
|
+
// If both insert and update have the same managed definition exclude the old value check
|
|
85
|
+
|| (onInsert && onUpdate && insert === update)
|
|
86
|
+
? `${insert} as ${name}`
|
|
87
|
+
: `!OLD.${keyZero} ? ${
|
|
88
|
+
// If key of old is null execute insert
|
|
89
|
+
insert
|
|
90
|
+
} : ${
|
|
91
|
+
// Else execute managed update or keep old if no new data if provided
|
|
92
|
+
onUpdate ? update : `(${this.managed_default(name, `OLD.${name}`, update)})`
|
|
93
|
+
} as ${name}`
|
|
94
|
+
)
|
|
95
|
+
if (c.upsert) c.upsert = '\n ' + c.upsert
|
|
96
|
+
})
|
|
97
|
+
return ret
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
managed_default(name, managed, src) {
|
|
101
|
+
return `!${src} ? ${managed} : ${src}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
managed_extract(name) {
|
|
105
|
+
const { UPSERT, INSERT } = this.cqn
|
|
106
|
+
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
107
|
+
? `value[${this.columns.indexOf(name)}]`
|
|
108
|
+
: `value[${JSON.stringify(name)}]`
|
|
109
|
+
const sql = extract
|
|
110
|
+
return { extract, sql }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
|
115
|
+
|
|
116
|
+
module.exports = CQN2PQLRenderer
|