@cap-js/sqlite 0.1.0 → 1.0.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 +9 -0
- package/README.md +1 -184
- package/cds-plugin.js +5 -0
- package/index.js +1 -1
- package/lib/ReservedWords.json +149 -0
- package/lib/SQLiteService.js +215 -0
- package/package.json +23 -27
- package/cds.js +0 -39
- package/lib/db/DatabaseService.js +0 -101
- package/lib/db/sql/InsertResults.js +0 -87
- package/lib/db/sql/SQLService.js +0 -223
- package/lib/db/sql/copy.js +0 -17
- package/lib/db/sql/cqn2sql.js +0 -515
- package/lib/db/sql/cqn4sql.js +0 -1461
- package/lib/db/sql/deep.js +0 -233
- package/lib/db/sql/func.js +0 -146
- package/lib/db/sql/structuralComparisonOps.js +0 -16
- package/lib/db/sql/utils.js +0 -22
- package/lib/db/sql/workarounds.js +0 -73
- package/lib/db/sqlite/ReservedWords.json +0 -149
- package/lib/db/sqlite/SQLiteService.js +0 -170
- package/lib/ql/cds.infer.js +0 -786
- package/lib/ql/join-tree.js +0 -167
- package/lib/ql/pseudos.js +0 -23
package/lib/ql/cds.infer.js
DELETED
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const cds = require('@sap/cds/lib');
|
|
4
|
-
|
|
5
|
-
const JoinTree = require('./join-tree');
|
|
6
|
-
const { pseudos } = require('./pseudos');
|
|
7
|
-
// REVISIT: we should always return cds.linked elements
|
|
8
|
-
const cdsTypes = cds.linked({
|
|
9
|
-
definitions: {
|
|
10
|
-
Timestamp: { type: 'cds.Timestamp' },
|
|
11
|
-
DateTime: { type: 'cds.DateTime' },
|
|
12
|
-
Date: { type: 'cds.Date' },
|
|
13
|
-
Time: { type: 'cds.Time' },
|
|
14
|
-
String: { type: 'cds.String' },
|
|
15
|
-
Decimal: { type: 'cds.Decimal' },
|
|
16
|
-
Integer: { type: 'cds.Integer' },
|
|
17
|
-
Boolean: { type: 'cds.Boolean' },
|
|
18
|
-
},
|
|
19
|
-
}).definitions;
|
|
20
|
-
for (const each in cdsTypes)
|
|
21
|
-
cdsTypes[`cds.${ each }`] = cdsTypes[each];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {CQN|CQL} originalQuery
|
|
25
|
-
* @param {CSN} [model]
|
|
26
|
-
* @returns {InferredCQN} = q with .target and .elements
|
|
27
|
-
*/
|
|
28
|
-
function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
29
|
-
if (!model)
|
|
30
|
-
cds.error('Please specify a model');
|
|
31
|
-
const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
32
|
-
|
|
33
|
-
// REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
|
|
34
|
-
// e.g. there's a lot of overhead for cds.inferred( SELECT.from(Books) )
|
|
35
|
-
if (originalQuery.SET)
|
|
36
|
-
cds.error('”UNION” based queries are not supported');
|
|
37
|
-
const _ = inferred.SELECT || inferred.INSERT || inferred.UPSERT || inferred.UPDATE || inferred.DELETE || inferred.CREATE || inferred.DROP;
|
|
38
|
-
const sources = inferTarget(_.from || _.into || _.entity, {});
|
|
39
|
-
const joinTree = new JoinTree(sources);
|
|
40
|
-
const aliases = Object.keys(sources);
|
|
41
|
-
Object.defineProperties(inferred, {
|
|
42
|
-
// REVISIT: public, or for local reuse, or in cqn4sql only?
|
|
43
|
-
sources: { value: sources, writable: true },
|
|
44
|
-
target: { value: aliases.length === 1 ? sources[aliases[0]] : originalQuery, writable: true }, // REVISIT: legacy?
|
|
45
|
-
});
|
|
46
|
-
// also enrich original query -> writable because it may be inferred again
|
|
47
|
-
Object.defineProperties(originalQuery, {
|
|
48
|
-
sources: { value: sources, writable: true },
|
|
49
|
-
target: {
|
|
50
|
-
value: aliases.length === 1 ? sources[aliases[0]] : originalQuery,
|
|
51
|
-
writable: true
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
|
|
55
|
-
const $combinedElements = inferCombinedElements();
|
|
56
|
-
/**
|
|
57
|
-
* TODO: this function is currently only called on DELETE's
|
|
58
|
-
* because it correctly set's up the $refLink's in the
|
|
59
|
-
* where clause: This functionality should be pulled out
|
|
60
|
-
* of ´inferQueryElement()` as this is a subtle side effect
|
|
61
|
-
*/
|
|
62
|
-
const elements = inferQueryElements($combinedElements);
|
|
63
|
-
Object.defineProperties(inferred, {
|
|
64
|
-
$combinedElements: { value: $combinedElements, writable: true },
|
|
65
|
-
elements: { value: elements, writable: true },
|
|
66
|
-
joinTree: { value: joinTree, writable: true }, // REVISIT: eliminate
|
|
67
|
-
});
|
|
68
|
-
// also enrich original query -> writable because it may be inferred again
|
|
69
|
-
Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true })
|
|
70
|
-
}
|
|
71
|
-
return inferred;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Infers all query sources from a queries `from` clause.
|
|
75
|
-
* Drills down into join arguments of the from clause.
|
|
76
|
-
*/
|
|
77
|
-
function inferTarget(from, querySources) {
|
|
78
|
-
const { ref } = from;
|
|
79
|
-
if (ref) {
|
|
80
|
-
const first = ref[0].id || ref[0];
|
|
81
|
-
let target = getDefinition(first, model);
|
|
82
|
-
if (!target)
|
|
83
|
-
cds.error(`"${ first }" not found in the definitions of your model`);
|
|
84
|
-
if (ref.length > 1) {
|
|
85
|
-
target = from.ref.slice(1).reduce(
|
|
86
|
-
(d, r) => {
|
|
87
|
-
const next = d.elements[r.id || r]?.elements
|
|
88
|
-
? d.elements[r.id || r] : d.elements[r.id || r]?._target;
|
|
89
|
-
if (!next)
|
|
90
|
-
cds.error(`No association "${ r.id || r }" in ${ d.kind } "${ d.name }": ${ d }`);
|
|
91
|
-
return next;
|
|
92
|
-
},
|
|
93
|
-
target
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
if (target.kind !== 'entity' && !target._isAssociation)
|
|
97
|
-
throw new Error(/Query source must be a an entity or an association/);
|
|
98
|
-
|
|
99
|
-
attachRefLinksToArg(from); // REVISIT: remove
|
|
100
|
-
const alias = from.as ||
|
|
101
|
-
(ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1]);
|
|
102
|
-
if (alias in querySources)
|
|
103
|
-
throw new Error(`Duplicate alias "${ alias }"`);
|
|
104
|
-
querySources[alias] = target;
|
|
105
|
-
}
|
|
106
|
-
else if (from.args) {
|
|
107
|
-
from.args.forEach(a => inferTarget(a, querySources));
|
|
108
|
-
}
|
|
109
|
-
else if (from.SELECT) {
|
|
110
|
-
infer(from, model) // we need the .elements in the sources
|
|
111
|
-
querySources[from.as] = from;
|
|
112
|
-
}
|
|
113
|
-
else if (typeof from === 'string') {
|
|
114
|
-
querySources[/([^.]*)$/.exec(from)[0]] = getDefinition(from, model);
|
|
115
|
-
}
|
|
116
|
-
else if (from.SET) {
|
|
117
|
-
infer(from, model);
|
|
118
|
-
}
|
|
119
|
-
return querySources;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// REVISIT: this helper is doing by far too much, with too many side effects
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Walk recursively through all `ref` steps of the `arg` and attach information such as
|
|
126
|
-
* the corresponding definition of each `ref` step as well as the target of the `ref` step
|
|
127
|
-
* in which the next `ref` step must be searched for in.
|
|
128
|
-
*
|
|
129
|
-
* @param {object} arg the arg which shall be augmented
|
|
130
|
-
* @param {$refLink} $baseLink environment where the first `ref` step shall be resolved in.
|
|
131
|
-
* For infix filter / expand columns
|
|
132
|
-
* @param {boolean} expand whether the `arg` is part of a `column.expand`
|
|
133
|
-
*/
|
|
134
|
-
function attachRefLinksToArg(arg, $baseLink = null, expand = false) {
|
|
135
|
-
const { ref, xpr } = arg;
|
|
136
|
-
if(xpr)
|
|
137
|
-
xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expand))
|
|
138
|
-
if (!ref)
|
|
139
|
-
return;
|
|
140
|
-
init$refLinks(arg);
|
|
141
|
-
ref.forEach((step, i) => {
|
|
142
|
-
const id = step.id || step;
|
|
143
|
-
if (i === 0) {
|
|
144
|
-
// infix filter never have table alias
|
|
145
|
-
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
146
|
-
if ($baseLink) {
|
|
147
|
-
const { definition } = $baseLink;
|
|
148
|
-
const elements = definition.elements || definition._target?.elements;
|
|
149
|
-
const e = elements?.[id] || cds.error`"${ id }" not found in the elements of "${ definition.name }"`;
|
|
150
|
-
if (e.target) { // only fk access in infix filter
|
|
151
|
-
const nextStep = ref[1]?.id || ref[1];
|
|
152
|
-
// no unmanaged assoc in infix filter path
|
|
153
|
-
if (!expand && e.on)
|
|
154
|
-
throw new Error(`"${ e.name }" in path "${ arg.ref.join('.') }" must not be an unmanaged association`);
|
|
155
|
-
// no non-fk traversal in infix filter
|
|
156
|
-
if (!expand && nextStep && !(nextStep in e.foreignKeys))
|
|
157
|
-
throw new Error(`Only foreign keys of "${ e.name }" can be accessed in infix filter`);
|
|
158
|
-
}
|
|
159
|
-
arg.$refLinks.push({ definition: e, target: e._target || e });
|
|
160
|
-
// filter paths are flattened
|
|
161
|
-
// REVISIT: too much augmentation -> better remove flatName..
|
|
162
|
-
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true });
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// must be in model.definitions
|
|
166
|
-
const definition = getDefinition(id, model);
|
|
167
|
-
arg.$refLinks[0] = { definition, target: definition };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
const recent = arg.$refLinks[i - 1];
|
|
172
|
-
const { elements } = recent.target;
|
|
173
|
-
const e = elements[id];
|
|
174
|
-
if (!e)
|
|
175
|
-
throw new Error(`"${ id }" not found in the elements of "${ arg.$refLinks[i - 1].definition.name }"`);
|
|
176
|
-
arg.$refLinks.push({ definition: e, target: e._target || e });
|
|
177
|
-
}
|
|
178
|
-
arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop();
|
|
179
|
-
|
|
180
|
-
// link refs in where
|
|
181
|
-
if (step.where) { // REVISIT: why do we need to walk through these so early?
|
|
182
|
-
if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target)
|
|
183
|
-
step.where.forEach(each => (each.ref || each.xpr) && attachRefLinksToArg(each, arg.$refLinks[i]));
|
|
184
|
-
else
|
|
185
|
-
throw new Error('A filter can only be provided when navigating along associations');
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Based on the queries `sources`, the `$combinedElements` are calculated.
|
|
192
|
-
* The `$combinedElements` of a query consist of all accessible elements
|
|
193
|
-
* across all the table aliases found in the from clause.
|
|
194
|
-
*
|
|
195
|
-
* The `$combinedElements` are attached to the query as non-enumerable property.
|
|
196
|
-
* Each entry in the `$combinedElements` dictionary maps from the element name
|
|
197
|
-
* to all table aliases where an element with this name can be found.
|
|
198
|
-
*/
|
|
199
|
-
function inferCombinedElements() {
|
|
200
|
-
const combinedElements = {};
|
|
201
|
-
for (const index in sources) {
|
|
202
|
-
const tableAlias = sources[index];
|
|
203
|
-
for (const key in tableAlias.elements) {
|
|
204
|
-
if (key in combinedElements)
|
|
205
|
-
combinedElements[key].push({ index, tableAlias });
|
|
206
|
-
else
|
|
207
|
-
combinedElements[key] = [ { index, tableAlias } ];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return combinedElements;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
215
|
-
*
|
|
216
|
-
* @param {object} col
|
|
217
|
-
* @param {csn.Element} element
|
|
218
|
-
*/
|
|
219
|
-
function setElementOnColumns(col, element) {
|
|
220
|
-
Object.defineProperty(col, 'element', {
|
|
221
|
-
value: element,
|
|
222
|
-
writable: true
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Walks over all columns of a queries `SELECT` and infers each `ref`, `xpr`
|
|
228
|
-
* or `val` as query element based on the queries `$combinedElements` and
|
|
229
|
-
* `sources`.
|
|
230
|
-
*
|
|
231
|
-
* The `elements` are attached to the query as non-enumerable property.
|
|
232
|
-
*
|
|
233
|
-
* Also walks over other `ref`s in the query, validates them and attaches `$refLinks`.
|
|
234
|
-
* --> `where`, infix filters within column refs or other csn paths...
|
|
235
|
-
*
|
|
236
|
-
*/
|
|
237
|
-
function inferQueryElements($combinedElements) {
|
|
238
|
-
let queryElements = {};
|
|
239
|
-
const {
|
|
240
|
-
columns, where, groupBy, having, orderBy,
|
|
241
|
-
} = _;
|
|
242
|
-
if (!columns) {
|
|
243
|
-
inferElementsFromWildCard(aliases);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
let wildcardSelect = false;
|
|
247
|
-
const refs = [];
|
|
248
|
-
columns.forEach((col) => {
|
|
249
|
-
if (col === '*') {
|
|
250
|
-
wildcardSelect = true;
|
|
251
|
-
} else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
|
|
252
|
-
const as = col.as || col.func || col.val;
|
|
253
|
-
if (as === undefined)
|
|
254
|
-
throw cds.error`Expecting expression to have an alias name`;
|
|
255
|
-
if (queryElements[as])
|
|
256
|
-
throw cds.error`Duplicate definition of element “${ as }”`;
|
|
257
|
-
if (col.xpr || col.SELECT) {
|
|
258
|
-
queryElements[as] = getElementForXprOrSubquery(col);
|
|
259
|
-
} else if (col.func) {
|
|
260
|
-
col.args?.forEach(arg => inferQueryElement(arg, false)); // {func}.args are optional
|
|
261
|
-
queryElements[as] = getElementForCast(col);
|
|
262
|
-
} else { // either binding parameter (col.param) or value
|
|
263
|
-
queryElements[as] = col.cast ? getElementForCast(col) : getCdsTypeForVal(col.val);
|
|
264
|
-
}
|
|
265
|
-
setElementOnColumns(col, queryElements[as])
|
|
266
|
-
} else if (col.ref) {
|
|
267
|
-
refs.push(col);
|
|
268
|
-
} else {
|
|
269
|
-
throw cds.error`Not supported: ${ JSON.stringify(col) }`;
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
refs.forEach(col => {
|
|
273
|
-
inferQueryElement(col)
|
|
274
|
-
const {definition} = col.$refLinks[col.$refLinks.length-1]
|
|
275
|
-
if(col.cast) // final type overwritten -> element not visible anymore
|
|
276
|
-
setElementOnColumns(col, getElementForCast(col))
|
|
277
|
-
else if(col.ref.length === 1 & col.ref[0] === '$user') // shortcut to $user.id
|
|
278
|
-
setElementOnColumns(col, queryElements[col.as || '$user'])
|
|
279
|
-
else
|
|
280
|
-
setElementOnColumns(col, definition)
|
|
281
|
-
});
|
|
282
|
-
if (wildcardSelect)
|
|
283
|
-
inferElementsFromWildCard(aliases);
|
|
284
|
-
}
|
|
285
|
-
if (orderBy) { // link $refLinks -> special name resolution rules for orderBy
|
|
286
|
-
orderBy.forEach((token) => {
|
|
287
|
-
let $baseLink;
|
|
288
|
-
// first check if token ref is resolvable in query elements
|
|
289
|
-
if (columns) {
|
|
290
|
-
const e = queryElements[token.ref?.[0]];
|
|
291
|
-
const isAssocExpand = e?.$assocExpand; // expand on structure can be addressed
|
|
292
|
-
if (e && !isAssocExpand)
|
|
293
|
-
$baseLink = { definition: { elements: queryElements }, target: inferred };
|
|
294
|
-
}
|
|
295
|
-
else { // fallback to elements of query source
|
|
296
|
-
$baseLink = null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
inferQueryElement(token, false, $baseLink);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
if (where) {
|
|
303
|
-
let skipJoins;
|
|
304
|
-
const walkTokenStream = (token) => {
|
|
305
|
-
if (token === 'exists') { // no joins for infix filters along `exists <path>`
|
|
306
|
-
skipJoins = true;
|
|
307
|
-
} else if(token.xpr) {
|
|
308
|
-
// don't miss an exists within an expression
|
|
309
|
-
token.xpr.forEach(walkTokenStream)
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
inferQueryElement(token, false, null, skipJoins);
|
|
313
|
-
skipJoins = false;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
where.forEach(walkTokenStream);
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
if (groupBy) // link $refLinks
|
|
320
|
-
groupBy.forEach(token => inferQueryElement(token, false));
|
|
321
|
-
if (having) // link $refLinks
|
|
322
|
-
having.forEach(token => inferQueryElement(token, false));
|
|
323
|
-
if(_.with) // consider UPDATE.with
|
|
324
|
-
Object.values(_.with).forEach(val => inferQueryElement(val, false))
|
|
325
|
-
|
|
326
|
-
return queryElements;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Infers an element of the query based on the given `column`
|
|
330
|
-
*
|
|
331
|
-
* attaches non-enumerable property `$refLinks` to the `column`
|
|
332
|
-
* which holds the corresponding artifact represented by the ref step
|
|
333
|
-
* at the same index. Based on the leaf artifact of the `ref` path, the queryElement
|
|
334
|
-
* is inferred.
|
|
335
|
-
*
|
|
336
|
-
* @param {object} column
|
|
337
|
-
* @param {object} [insertIntoQueryElements=true]
|
|
338
|
-
* whether the inferred element shall be inserted into the queries elements.
|
|
339
|
-
* E.g. we do not want to do that when we walk over the where clause.
|
|
340
|
-
* @param {boolean} [inExists=false]
|
|
341
|
-
* In some cases, no joins must be created for non-assoc path traversals:
|
|
342
|
-
* - for infix filters in `exists assoc[parent.foo='bar']` -> part of semi join
|
|
343
|
-
*/
|
|
344
|
-
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, inExists = false, inInline = false) {
|
|
345
|
-
if (column.param)
|
|
346
|
-
return; // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
347
|
-
if (column.args)
|
|
348
|
-
column.args.forEach( arg => inferQueryElement(arg, false, $baseLink, inExists) ); // e.g. function in expression
|
|
349
|
-
if (column.list)
|
|
350
|
-
column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, inExists));
|
|
351
|
-
if (column.xpr)
|
|
352
|
-
column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, inExists)); // e.g. function in expression
|
|
353
|
-
if (column.SELECT)
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
if (!column.ref)
|
|
357
|
-
return;
|
|
358
|
-
|
|
359
|
-
init$refLinks(column);
|
|
360
|
-
|
|
361
|
-
const firstStepIsTableAlias = column.ref.length > 1 && column.ref[0] in sources ||
|
|
362
|
-
// nested projection on table alias
|
|
363
|
-
column.ref.length === 1 && column.ref[0] in sources && (column.inline);
|
|
364
|
-
const firstStepIsSelf
|
|
365
|
-
= !firstStepIsTableAlias && column.ref.length > 1 && [ '$self', '$projection' ].includes(column.ref[0]);
|
|
366
|
-
const nameSegments = [];
|
|
367
|
-
let pseudoPath = false;
|
|
368
|
-
column.ref.forEach((step, i) => {
|
|
369
|
-
const id = step.id || step;
|
|
370
|
-
if (i === 0) {
|
|
371
|
-
if (id in pseudos.elements) { // pseudo path
|
|
372
|
-
column.$refLinks.push({ definition: pseudos.elements[id], target: pseudos });
|
|
373
|
-
pseudoPath = true; // only first path step must be well defined
|
|
374
|
-
nameSegments.push(id);
|
|
375
|
-
}
|
|
376
|
-
else if ($baseLink) {
|
|
377
|
-
const { definition, target } = $baseLink;
|
|
378
|
-
const elements = definition.elements || definition._target?.elements;
|
|
379
|
-
if (elements && id in elements) {
|
|
380
|
-
if (!inExists && isStepJoinRelevant(definition, id, inInline ? null : $baseLink))
|
|
381
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
382
|
-
column.$refLinks.push({ definition: elements[id], target });
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
stepNotFoundInPredecessor(id, definition.name);
|
|
386
|
-
}
|
|
387
|
-
nameSegments.push(id);
|
|
388
|
-
}
|
|
389
|
-
else if (firstStepIsTableAlias) {
|
|
390
|
-
column.$refLinks.push({ definition: sources[id], target: sources[id] });
|
|
391
|
-
}
|
|
392
|
-
else if (firstStepIsSelf) {
|
|
393
|
-
column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } });
|
|
394
|
-
}
|
|
395
|
-
else if (id in $combinedElements) {
|
|
396
|
-
if ($combinedElements[id].length > 1)
|
|
397
|
-
stepIsAmbiguous(id); // exit
|
|
398
|
-
const definition = $combinedElements[id][0].tableAlias.elements[id];
|
|
399
|
-
const $refLink = { definition, target: $combinedElements[id][0].tableAlias };
|
|
400
|
-
column.$refLinks.push($refLink);
|
|
401
|
-
nameSegments.push(id);
|
|
402
|
-
}
|
|
403
|
-
else if (inferred.outerQueries) { // outer query accessed via alias
|
|
404
|
-
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources);
|
|
405
|
-
if (!outerAlias)
|
|
406
|
-
stepNotFoundInCombinedElements(id);
|
|
407
|
-
column.$refLinks.push({ definition: outerAlias.sources[id], target: outerAlias.sources[id] });
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
stepNotFoundInCombinedElements(id); // REVISIT: fails with {__proto__:elements)
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
const { definition } = column.$refLinks[i - 1];
|
|
415
|
-
const elements = definition.elements || definition._target?.elements;
|
|
416
|
-
if (elements && id in elements) {
|
|
417
|
-
const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target };
|
|
418
|
-
if (!inExists && isStepJoinRelevant(definition, id))
|
|
419
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
420
|
-
column.$refLinks.push($refLink);
|
|
421
|
-
}
|
|
422
|
-
else if (firstStepIsSelf) {
|
|
423
|
-
stepNotFoundInColumnList(id);
|
|
424
|
-
}
|
|
425
|
-
else if (column.ref[0] === '$user' && pseudoPath) {
|
|
426
|
-
// `$user.some.unknown.element` -> no error
|
|
427
|
-
column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target });
|
|
428
|
-
}
|
|
429
|
-
else if (id === '$dummy') {
|
|
430
|
-
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
431
|
-
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } });
|
|
432
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
const notFoundIn = pseudoPath ? column.ref[i - 1] : getFullPathForLinkedArg(column);
|
|
436
|
-
stepNotFoundInPredecessor(id, notFoundIn);
|
|
437
|
-
}
|
|
438
|
-
const foreignKeyAlias = Array.isArray(definition.keys)
|
|
439
|
-
? definition.keys.find(k => k.ref[0] === id )?.as : null;
|
|
440
|
-
nameSegments.push(foreignKeyAlias || id);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (step.where) {
|
|
444
|
-
const danglingFilter = !(column.ref[i + 1] || column.expand || inExists);
|
|
445
|
-
if (!column.$refLinks[i].definition.target || danglingFilter)
|
|
446
|
-
throw new Error(/A filter can only be provided when navigating along associations/);
|
|
447
|
-
if (!column.expand)
|
|
448
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
449
|
-
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
450
|
-
let skipJoinsForFilter = inExists;
|
|
451
|
-
step.where.forEach( (token) => {
|
|
452
|
-
if (token === 'exists') { // no joins for infix filters along `exists <path>`
|
|
453
|
-
skipJoinsForFilter = true;
|
|
454
|
-
}
|
|
455
|
-
else if (token.ref || token.xpr) {
|
|
456
|
-
inferQueryElement(token, false, column.$refLinks[i], skipJoinsForFilter);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop();
|
|
462
|
-
|
|
463
|
-
if (!column.ref[i + 1]) {
|
|
464
|
-
const flatName = nameSegments.join('_');
|
|
465
|
-
Object.defineProperty(column, 'flatName', { value: flatName, writable: true });
|
|
466
|
-
// if column is casted, we overwrite it's origin with the new type
|
|
467
|
-
if (column.cast) {
|
|
468
|
-
const base = getElementForCast(column);
|
|
469
|
-
if (insertIntoQueryElements)
|
|
470
|
-
queryElements[column.as || flatName] = getCopyWithAnnos(column, base);
|
|
471
|
-
}
|
|
472
|
-
else if (column.expand) {
|
|
473
|
-
const elements = resolveExpand(column);
|
|
474
|
-
if (insertIntoQueryElements)
|
|
475
|
-
queryElements[column.as || flatName] = elements;
|
|
476
|
-
}
|
|
477
|
-
else if (column.inline && insertIntoQueryElements) {
|
|
478
|
-
const elements = resolveInline(column);
|
|
479
|
-
queryElements = { ...queryElements, ...elements };
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
// shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
|
|
483
|
-
const leafArt
|
|
484
|
-
= i === 0 && id === '$user' ? column.$refLinks[i].definition.elements.id : column.$refLinks[i].definition;
|
|
485
|
-
// infer element based on leaf artifact of path
|
|
486
|
-
if (insertIntoQueryElements) {
|
|
487
|
-
const name = column.as || flatName;
|
|
488
|
-
if (queryElements[name] !== undefined)
|
|
489
|
-
throw new Error(`Duplicate definition of element “${ name }”`);
|
|
490
|
-
queryElements[name] = getCopyWithAnnos(column, leafArt);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
497
|
-
if (column.expand) {
|
|
498
|
-
const { $refLinks } = column;
|
|
499
|
-
const skip = $refLinks
|
|
500
|
-
.some(link => model.definitions[link.definition.target]?.['@cds.persistence.skip'] === true);
|
|
501
|
-
if (skip) {
|
|
502
|
-
$refLinks[$refLinks.length - 1].skipExpand = true;
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (!inExists && column.isJoinRelevant)
|
|
508
|
-
joinTree.mergeColumn(column);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
512
|
-
const { inline, $refLinks } = col;
|
|
513
|
-
const $leafLink = $refLinks[$refLinks.length - 1];
|
|
514
|
-
let elements = {};
|
|
515
|
-
inline.forEach((inlineCol) => {
|
|
516
|
-
inferQueryElement(inlineCol, false, $leafLink, false, true);
|
|
517
|
-
if (inlineCol === '*') {
|
|
518
|
-
const wildCardElements = {};
|
|
519
|
-
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
520
|
-
const leafLinkElements = $leafLink.definition.elements || $leafLink.definition._target.elements
|
|
521
|
-
Object.entries(leafLinkElements).forEach(([ k, v ]) => {
|
|
522
|
-
const name = namePrefix ? `${ namePrefix }_${ k }` : k;
|
|
523
|
-
// if overwritten/excluded omit from wildcard elements
|
|
524
|
-
// in elements the names are already flat so consider the prefix
|
|
525
|
-
// in excluding, the elements are addressed without the prefix
|
|
526
|
-
if (!(name in elements || col.excluding?.some(e => e === k)))
|
|
527
|
-
wildCardElements[name] = v;
|
|
528
|
-
});
|
|
529
|
-
elements = { ...elements, ...wildCardElements };
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
const nameParts = namePrefix ? [namePrefix] : []
|
|
533
|
-
if(inlineCol.as)
|
|
534
|
-
nameParts.push(inlineCol.as)
|
|
535
|
-
else
|
|
536
|
-
nameParts.push(...inlineCol.ref.map(idOnly))
|
|
537
|
-
const name = nameParts.join('_')
|
|
538
|
-
if (inlineCol.inline) {
|
|
539
|
-
const inlineElements = resolveInline(inlineCol, name);
|
|
540
|
-
elements = { ...elements, ...inlineElements };
|
|
541
|
-
}
|
|
542
|
-
else if (inlineCol.expand) {
|
|
543
|
-
const expandElements = resolveExpand(inlineCol);
|
|
544
|
-
elements = { ...elements, [name]: expandElements };
|
|
545
|
-
}
|
|
546
|
-
else if (inlineCol.val) {
|
|
547
|
-
elements[name] = {...getCdsTypeForVal(inlineCol.val)}
|
|
548
|
-
}
|
|
549
|
-
else if (inlineCol.func) {
|
|
550
|
-
elements[name] = {}
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
return elements;
|
|
558
|
-
}
|
|
559
|
-
function resolveExpand(col) {
|
|
560
|
-
const { expand, $refLinks } = col;
|
|
561
|
-
const $leafLink = $refLinks[$refLinks.length - 1];
|
|
562
|
-
if ($leafLink.definition._target) {
|
|
563
|
-
const expandSubquery = {
|
|
564
|
-
SELECT: {
|
|
565
|
-
from: $leafLink.definition._target.name,
|
|
566
|
-
columns: expand.filter(c => !c.inline),
|
|
567
|
-
},
|
|
568
|
-
};
|
|
569
|
-
if (col.as)
|
|
570
|
-
expandSubquery.SELECT.as = col.as;
|
|
571
|
-
const inferredExpandSubquery = infer(expandSubquery, model);
|
|
572
|
-
const res = $leafLink.definition._isStructured || $leafLink.definition.is2one
|
|
573
|
-
// IMPORTANT: all definitions / elements in a cds.linked model have to be linked
|
|
574
|
-
? new cds.struct ({ elements: inferredExpandSubquery.elements })
|
|
575
|
-
: new cds.array ({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) });
|
|
576
|
-
return Object.defineProperty(res, '$assocExpand', { value: true });
|
|
577
|
-
} // struct
|
|
578
|
-
let elements = {};
|
|
579
|
-
expand.forEach((e) => {
|
|
580
|
-
if (e === '*') {
|
|
581
|
-
elements = { ...elements, ...$leafLink.definition.elements };
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
inferQueryElement(e, false, $leafLink);
|
|
585
|
-
if (e.expand)
|
|
586
|
-
elements[e.as || e.flatName] = resolveExpand(e);
|
|
587
|
-
if (e.inline)
|
|
588
|
-
elements = { ...elements, ...resolveInline(e) };
|
|
589
|
-
|
|
590
|
-
else
|
|
591
|
-
elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e;
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
return new cds.struct({ elements });
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function stepNotFoundInPredecessor(step, def) {
|
|
598
|
-
throw new Error(`"${ step }" not found in "${ def }"`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function stepIsAmbiguous(step) {
|
|
602
|
-
throw new Error(
|
|
603
|
-
`ambiguous reference to "${ step }", write ${ Object.values($combinedElements[step])
|
|
604
|
-
.map(ta => `"${ ta.index }.${ step }"`)
|
|
605
|
-
.join(', ') } instead`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function stepNotFoundInCombinedElements(step) {
|
|
610
|
-
throw new Error(
|
|
611
|
-
`"${ step }" not found in the elements of ${ Object.values(sources)
|
|
612
|
-
.map(def => `"${ def.name || /* subquery */ def.as }"`)
|
|
613
|
-
.join(', ') }`
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function stepNotFoundInColumnList(step) {
|
|
618
|
-
const err = [ `"${ step }" not found in the columns list of query` ];
|
|
619
|
-
// if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
|
|
620
|
-
if (step in $combinedElements)
|
|
621
|
-
err.push(` did you mean ${ $combinedElements[step].map(ta => `"${ ta.index || ta.as }.${ step }"`).join(',') }?`);
|
|
622
|
-
throw new Error(err);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function isStepJoinRelevant(definition, id, inInfixFilter = null) {
|
|
627
|
-
return (definition.on || Array.isArray(definition.keys) &&
|
|
628
|
-
/* infix filter in column always join relevant, even if fk access */
|
|
629
|
-
(inInfixFilter || !definition.keys.some(fk => fk.as === id || fk.ref[fk.ref.length - 1] === id)));
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
|
|
634
|
-
* if there is not already an element with the same name present.
|
|
635
|
-
*/
|
|
636
|
-
function inferElementsFromWildCard() {
|
|
637
|
-
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
638
|
-
// only one query source and no overwritten columns
|
|
639
|
-
queryElements = sources[aliases[0]].elements;
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false;
|
|
644
|
-
const ambiguousElements = {};
|
|
645
|
-
Object.entries($combinedElements).forEach(([ name, tableAliases ]) => {
|
|
646
|
-
if (Object.keys(tableAliases).length > 1) {
|
|
647
|
-
ambiguousElements[name] = tableAliases;
|
|
648
|
-
return ambiguousElements[name];
|
|
649
|
-
}
|
|
650
|
-
if (exclude(name) || name in queryElements)
|
|
651
|
-
return true;
|
|
652
|
-
queryElements[name] = tableAliases[0].tableAlias.elements[name];
|
|
653
|
-
return queryElements[name];
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
if (Object.keys(ambiguousElements).length > 0)
|
|
657
|
-
throwAmbiguousWildcardError();
|
|
658
|
-
|
|
659
|
-
function throwAmbiguousWildcardError() {
|
|
660
|
-
const err = [];
|
|
661
|
-
err.push('Ambiguous wildcard elements:');
|
|
662
|
-
Object.keys(ambiguousElements).forEach((name) => {
|
|
663
|
-
const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index);
|
|
664
|
-
err.push(
|
|
665
|
-
` select "${ name }" explicitly with ${ tableAliasNames.map(taName => `"${ taName }.${ name }"`).join(', ') }`
|
|
666
|
-
);
|
|
667
|
-
});
|
|
668
|
-
throw new Error(err.join('\n'));
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Returns a new object which is the inferred element for the given `col`.
|
|
674
|
-
* A cast type (via cast function) on the column gets preserved.
|
|
675
|
-
*
|
|
676
|
-
* @param {object} col
|
|
677
|
-
* @returns object
|
|
678
|
-
*/
|
|
679
|
-
function getElementForXprOrSubquery(col) {
|
|
680
|
-
const { xpr } = col;
|
|
681
|
-
let skipJoins = false;
|
|
682
|
-
xpr?.forEach( (token) => {
|
|
683
|
-
if (token === 'exists') { // no joins for infix filters along `exists <path>`
|
|
684
|
-
skipJoins = true;
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
inferQueryElement(token, false, null, skipJoins);
|
|
688
|
-
skipJoins = false;
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
const base = getElementForCast(col.cast ? col : xpr?.[0] || col);
|
|
692
|
-
if (col.key)
|
|
693
|
-
base.key = col.key; // > preserve key on column
|
|
694
|
-
return getCopyWithAnnos(col, base);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Returns an object with the cast-type defined in the cast of the `thing`.
|
|
699
|
-
* If no cast property is present, it just returns an empty object.
|
|
700
|
-
* The type of the cast is mapped to the `cds` type if possible.
|
|
701
|
-
*
|
|
702
|
-
* @param {object} thing with the cast property
|
|
703
|
-
* @returns {object}
|
|
704
|
-
*/
|
|
705
|
-
function getElementForCast(thing) {
|
|
706
|
-
const { cast, $refLinks } = thing;
|
|
707
|
-
if (!cast)
|
|
708
|
-
return {};
|
|
709
|
-
if($refLinks?.[$refLinks.length - 1].definition.elements) // no cast on structure
|
|
710
|
-
cds.error`Structured elements can't be cast to a different type`;
|
|
711
|
-
thing.cast = cdsTypes[cast.type] || cast;
|
|
712
|
-
return thing.cast;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* return a new object based on @param base
|
|
718
|
-
* with all annotations found in @param from
|
|
719
|
-
*
|
|
720
|
-
* @param {object} from
|
|
721
|
-
* @param {object} base
|
|
722
|
-
* @returns {object} a copy of @param base with all annotations of @param from
|
|
723
|
-
* @TODO prototype based
|
|
724
|
-
*/
|
|
725
|
-
// REVISIT: TODO: inferred.elements should be linked
|
|
726
|
-
function getCopyWithAnnos(from, base) {
|
|
727
|
-
const result = { ...base };
|
|
728
|
-
// REVISIT: we don't need to and hence should not handle annotations at runtime
|
|
729
|
-
for (const prop in from) {
|
|
730
|
-
if (prop.startsWith('@'))
|
|
731
|
-
result[prop] = from[prop];
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (from.as && base.name !== from.as)
|
|
735
|
-
Object.defineProperty(result, 'name', { value: from.as }); // TODO double check if this is needed
|
|
736
|
-
// in subqueries we need the linked element if an outer query accesses it
|
|
737
|
-
return Object.setPrototypeOf(result, base);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// REVISIT: functions without return are by nature side-effect functions -> bad
|
|
741
|
-
function init$refLinks(arg) {
|
|
742
|
-
Object.defineProperty(arg, '$refLinks', {
|
|
743
|
-
value: [],
|
|
744
|
-
writable: true,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function getCdsTypeForVal(val) {
|
|
749
|
-
// REVISIT: JS null should have a type for proper DB layer conversion logic
|
|
750
|
-
// if(val === null) return {type:'cds.String'}
|
|
751
|
-
switch (typeof val) {
|
|
752
|
-
case 'string': return cdsTypes.String;
|
|
753
|
-
case 'boolean': return cdsTypes.Boolean;
|
|
754
|
-
case 'number': return Number.isSafeInteger(val) ? cdsTypes.Integer : cdsTypes.Decimal;
|
|
755
|
-
default: return {};
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/** gets the CSN element for the given name from the model */
|
|
760
|
-
function getDefinition(name, model) {
|
|
761
|
-
return model.definitions[name] || cds.error`"${ name }" not found in the definitions of your model`;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Returns the csn path as string for a given column ref with sibling $refLinks
|
|
766
|
-
*
|
|
767
|
-
* @param {object} arg
|
|
768
|
-
* @returns {string}
|
|
769
|
-
*/
|
|
770
|
-
function getFullPathForLinkedArg(arg) {
|
|
771
|
-
let firstStepIsEntity = false;
|
|
772
|
-
return arg.$refLinks.reduce((res, cur, i) => {
|
|
773
|
-
if (cur.definition.kind === 'entity') {
|
|
774
|
-
firstStepIsEntity = true;
|
|
775
|
-
if (arg.$refLinks.length === 1)
|
|
776
|
-
return `${ cur.definition.name }`;
|
|
777
|
-
return `${ cur.definition.name }`;
|
|
778
|
-
}
|
|
779
|
-
const dot = i === 1 && firstStepIsEntity ? ':' : '.'; // divide with colon if first step is entity
|
|
780
|
-
return res !== '' ? res + dot + cur.definition.name : cur.definition.name;
|
|
781
|
-
}, '');
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
const idOnly = (ref) => ref.id || ref
|
|
785
|
-
|
|
786
|
-
module.exports = infer;
|