@sap/cds-compiler 5.3.2 → 5.4.2
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 +29 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_BETA.md +2 -2
- package/lib/api/options.js +4 -2
- package/lib/base/builtins.js +0 -10
- package/lib/base/keywords.js +3 -31
- package/lib/base/message-registry.js +23 -5
- package/lib/base/messages.js +1 -1
- package/lib/checks/existsMustEndInAssoc.js +7 -2
- package/lib/checks/foreignKeys.js +12 -7
- package/lib/compiler/assert-consistency.js +11 -3
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +88 -38
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/shared.js +9 -10
- package/lib/compiler/xpr-rewrite.js +11 -0
- package/lib/compiler/xsn-model.js +1 -1
- package/lib/edm/csn2edm.js +2 -0
- package/lib/edm/edm.js +2 -1
- package/lib/edm/edmPreprocessor.js +14 -1
- package/lib/edm/edmUtils.js +17 -2
- package/lib/gen/BaseParser.js +291 -197
- package/lib/gen/CdlParser.js +1631 -1605
- package/lib/gen/Dictionary.json +74 -6
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +1808 -1804
- package/lib/language/antlrParser.js +8 -4
- package/lib/language/genericAntlrParser.js +3 -3
- package/lib/model/csnUtils.js +6 -1
- package/lib/optionProcessor.js +4 -0
- package/lib/parsers/AstBuildingParser.js +172 -108
- package/lib/parsers/CdlGrammar.g4 +154 -134
- package/lib/parsers/Lexer.js +3 -3
- package/lib/parsers/identifiers.js +59 -0
- package/lib/render/toCdl.js +5 -5
- package/lib/render/utils/common.js +5 -0
- package/lib/render/utils/delta.js +23 -5
- package/lib/transform/db/expansion.js +2 -1
- package/lib/transform/db/transformExists.js +10 -9
- package/lib/transform/effective/annotations.js +147 -0
- package/lib/transform/effective/main.js +16 -2
- package/lib/transform/forOdata.js +53 -10
- package/lib/transform/forRelationalDB.js +7 -0
- package/lib/transform/odata/createForeignKeys.js +180 -0
- package/lib/transform/odata/flattening.js +135 -19
- package/lib/transform/odata/typesExposure.js +4 -3
- package/lib/transform/transformUtils.js +6 -6
- package/package.json +1 -1
package/lib/parsers/Lexer.js
CHANGED
|
@@ -177,9 +177,9 @@ function string( text, lexer, parser, beg ) {
|
|
|
177
177
|
}
|
|
178
178
|
else { // try with previous date/time/timestamp/x
|
|
179
179
|
prefix = parser.tokens[parser.tokens.length - 1];
|
|
180
|
-
if (prefix.location.endLine !== lexer.location.line ||
|
|
180
|
+
if (prefix && (prefix.location.endLine !== lexer.location.line ||
|
|
181
181
|
prefix.location.endCol !== lexer.location.col ||
|
|
182
|
-
!quotedLiterals.includes( prefix.keyword ))
|
|
182
|
+
!quotedLiterals.includes( prefix.keyword )))
|
|
183
183
|
prefix = null;
|
|
184
184
|
while (re.test( lexer.input ) && lexer.input[re.lastIndex] === "'")
|
|
185
185
|
esc = ++re.lastIndex;
|
|
@@ -192,7 +192,7 @@ function string( text, lexer, parser, beg ) {
|
|
|
192
192
|
const before = (lastIndex) ? 'string' : 'multi';
|
|
193
193
|
// eslint-disable-next-line cds-compiler/message-texts
|
|
194
194
|
parser.error( 'syntax-missing-token-end', lexer.location,
|
|
195
|
-
{ '#': before,
|
|
195
|
+
{ '#': before, newcode: text }, {
|
|
196
196
|
string: 'The string literal must end with $(NEWCODE) before the end of line',
|
|
197
197
|
multi: 'The multi-line string literal must end with $(NEWCODE)',
|
|
198
198
|
} );
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** RegEx identifying undelimited identifiers in CDL */
|
|
4
|
+
const undelimitedIdentifierRegex = /^[$_\p{ID_Start}][$\p{ID_Continue}\u200C\u200D]*$/u;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Functions without parentheses in CDL (common standard SQL-92 functions)
|
|
8
|
+
* (do not add more - make it part of the SQL renderer to remove parentheses for
|
|
9
|
+
* other funny SQL functions like CURRENT_UTCTIMESTAMP).
|
|
10
|
+
*/
|
|
11
|
+
const functionsWithoutParentheses = [
|
|
12
|
+
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
|
|
13
|
+
'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// CDL reserved keywords, used for automatic quoting in 'toCdl' renderer
|
|
17
|
+
// Keep in sync with reserved keywords in language.g4
|
|
18
|
+
const cdlKeywords = [
|
|
19
|
+
'ALL',
|
|
20
|
+
'ANY',
|
|
21
|
+
'AS',
|
|
22
|
+
'BY',
|
|
23
|
+
'CASE',
|
|
24
|
+
'CAST',
|
|
25
|
+
'DISTINCT',
|
|
26
|
+
'EXISTS',
|
|
27
|
+
'EXTRACT',
|
|
28
|
+
'FALSE', // boolean
|
|
29
|
+
'FROM',
|
|
30
|
+
'IN',
|
|
31
|
+
'KEY',
|
|
32
|
+
'NEW',
|
|
33
|
+
'NOT',
|
|
34
|
+
'NULL',
|
|
35
|
+
'OF',
|
|
36
|
+
'ON',
|
|
37
|
+
'SELECT',
|
|
38
|
+
'SOME',
|
|
39
|
+
'TRIM',
|
|
40
|
+
'TRUE', // boolean
|
|
41
|
+
'WHEN',
|
|
42
|
+
'WHERE',
|
|
43
|
+
'WITH',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function isSimpleCdlIdentifier( id ) {
|
|
47
|
+
if (undelimitedIdentifierRegex.test(id))
|
|
48
|
+
return true;
|
|
49
|
+
const upperId = id.toUpperCase();
|
|
50
|
+
return !cdlKeywords.includes(upperId) &&
|
|
51
|
+
!functionsWithoutParentheses.includes(upperId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
undelimitedIdentifierRegex,
|
|
56
|
+
cdlKeywords,
|
|
57
|
+
functionsWithoutParentheses,
|
|
58
|
+
isSimpleCdlIdentifier,
|
|
59
|
+
};
|
package/lib/render/toCdl.js
CHANGED
|
@@ -18,8 +18,8 @@ const {
|
|
|
18
18
|
const { isBuiltinType } = require('../base/builtins');
|
|
19
19
|
const { cloneFullCsn } = require('../model/cloneCsn');
|
|
20
20
|
const { getKeysDict } = require('../model/csnRefs');
|
|
21
|
+
const { undelimitedIdentifierRegex } = require('../parsers/identifiers');
|
|
21
22
|
|
|
22
|
-
const identifierRegex = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
|
|
23
23
|
const specialFunctionKeywords = Object.create(null);
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -2104,7 +2104,7 @@ function csnToCdl( csn, options, msg ) {
|
|
|
2104
2104
|
return path.split('.').map((step, index) => {
|
|
2105
2105
|
if (index === 0)
|
|
2106
2106
|
return quoteNonIdentifierOrKeyword(step, env);
|
|
2107
|
-
else if (!
|
|
2107
|
+
else if (!undelimitedIdentifierRegex.test(step))
|
|
2108
2108
|
return delimitedId(step, env);
|
|
2109
2109
|
return step;
|
|
2110
2110
|
}).join('.');
|
|
@@ -2142,7 +2142,7 @@ function csnToCdl( csn, options, msg ) {
|
|
|
2142
2142
|
* @return {string}
|
|
2143
2143
|
*/
|
|
2144
2144
|
function quoteNonIdentifier( id, env ) {
|
|
2145
|
-
if (!
|
|
2145
|
+
if (!undelimitedIdentifierRegex.test(id))
|
|
2146
2146
|
return delimitedId(id, env);
|
|
2147
2147
|
return id;
|
|
2148
2148
|
}
|
|
@@ -2262,7 +2262,7 @@ function removeTrailingNewline( str ) {
|
|
|
2262
2262
|
* @return {boolean}
|
|
2263
2263
|
*/
|
|
2264
2264
|
function requiresQuotingForCdl( id, additionalKeywords ) {
|
|
2265
|
-
return !
|
|
2265
|
+
return !undelimitedIdentifierRegex.test(id) ||
|
|
2266
2266
|
keywords.cdl.includes(id.toUpperCase()) ||
|
|
2267
2267
|
keywords.cdl_functions.includes(id.toUpperCase()) ||
|
|
2268
2268
|
additionalKeywords.includes(id.toUpperCase());
|
|
@@ -2523,7 +2523,7 @@ function apiSmartId( id, insideFunction = null ) {
|
|
|
2523
2523
|
*/
|
|
2524
2524
|
function apiSmartFunctionId( funcName ) {
|
|
2525
2525
|
const funcId = funcName.toUpperCase();
|
|
2526
|
-
const requiresQuoting = !
|
|
2526
|
+
const requiresQuoting = !undelimitedIdentifierRegex.test(funcName) ||
|
|
2527
2527
|
(keywords.cdl.includes(funcId) && !specialFunctions[funcId]);
|
|
2528
2528
|
if (requiresQuoting)
|
|
2529
2529
|
return apiDelimitedId(funcName);
|
|
@@ -263,6 +263,7 @@ const cdsToSqlTypes = {
|
|
|
263
263
|
'cds.hana.ST_POINT': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
|
|
264
264
|
'cds.hana.ST_GEOMETRY': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
|
|
265
265
|
'cds.Vector': 'NVARCHAR', // Not supported; see #11725
|
|
266
|
+
'cds.Map': 'NCLOB', // Not supported; see #13149
|
|
266
267
|
},
|
|
267
268
|
hana: {
|
|
268
269
|
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
|
|
@@ -270,6 +271,7 @@ const cdsToSqlTypes = {
|
|
|
270
271
|
'cds.hana.ST_POINT': 'ST_POINT',
|
|
271
272
|
'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',
|
|
272
273
|
'cds.Vector': 'REAL_VECTOR', // FIXME: test me
|
|
274
|
+
'cds.Map': 'NCLOB',
|
|
273
275
|
},
|
|
274
276
|
sqlite: {
|
|
275
277
|
'cds.Date': 'DATE_TEXT',
|
|
@@ -280,6 +282,7 @@ const cdsToSqlTypes = {
|
|
|
280
282
|
'cds.hana.BINARY': 'BINARY_BLOB',
|
|
281
283
|
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
|
|
282
284
|
'cds.Vector': 'BINARY_BLOB', // Not supported; see #11725
|
|
285
|
+
'cds.Map': 'JSON_TEXT', // '_TEXT' suffix required for text affinity
|
|
283
286
|
},
|
|
284
287
|
plain: {
|
|
285
288
|
'cds.Binary': 'VARBINARY',
|
|
@@ -292,6 +295,7 @@ const cdsToSqlTypes = {
|
|
|
292
295
|
'cds.DecimalFloat': 'DECFLOAT', // Decimal and Decimal(p) is mapped to cds.DecimalFloat
|
|
293
296
|
'cds.DateTime': 'TIMESTAMP(0)',
|
|
294
297
|
'cds.Timestamp': 'TIMESTAMP(7)',
|
|
298
|
+
'cds.Map': 'JSON',
|
|
295
299
|
},
|
|
296
300
|
postgres: {
|
|
297
301
|
// See <https://www.postgresql.org/docs/current/datatype.html>
|
|
@@ -302,6 +306,7 @@ const cdsToSqlTypes = {
|
|
|
302
306
|
'cds.Double': 'FLOAT8',
|
|
303
307
|
'cds.UInt8': 'INTEGER', // Not equivalent
|
|
304
308
|
'cds.Vector': 'VARCHAR', // Not supported; see #11725
|
|
309
|
+
'cds.Map': 'JSONB',
|
|
305
310
|
},
|
|
306
311
|
};
|
|
307
312
|
|
|
@@ -179,10 +179,8 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
179
179
|
*/
|
|
180
180
|
alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
|
|
181
181
|
const sqls = [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
else if (delta.new.notNull === false || delta.new.$notNull === false)
|
|
185
|
-
definitionsStr = definitionsStr.replace(' NULL', ''); // TODO: Is this robust enough?
|
|
182
|
+
|
|
183
|
+
definitionsStr = this.#removeNullabilityFromElementString(delta, definitionsStr);
|
|
186
184
|
|
|
187
185
|
if (delta.old.default && !delta.old.value) // Drop old default if any exists
|
|
188
186
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`);
|
|
@@ -190,7 +188,7 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
190
188
|
if (delta.new.default && !delta.new.value ) { // Alter column with default
|
|
191
189
|
const df = delta.new.default;
|
|
192
190
|
delete delta.new.default;
|
|
193
|
-
const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env);
|
|
191
|
+
const eltStrNoDefault = this.#removeNullabilityFromElementString(delta, this.scopedFunctions.renderElement(eltName, delta.new, null, null, env));
|
|
194
192
|
delta.new.default = df;
|
|
195
193
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`);
|
|
196
194
|
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`);
|
|
@@ -206,6 +204,26 @@ class DeltaRendererPostgres extends DeltaRenderer {
|
|
|
206
204
|
|
|
207
205
|
return sqls;
|
|
208
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Postgres does not support changing a column AND doing [NOT] NULL things in one statement.
|
|
210
|
+
* So we filter it from the SQL String and render the appropriate SET/DROP for the NOT NULL separately.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} delta
|
|
213
|
+
* @param {string} string
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
#removeNullabilityFromElementString(delta, string) {
|
|
217
|
+
if (delta.new.notNull === true || delta.new.key === true)
|
|
218
|
+
string = string.replace(' NOT NULL', ''); // TODO: Is this robust enough?
|
|
219
|
+
else if (delta.new.notNull === false || delta.new.$notNull === false)
|
|
220
|
+
string = string.replace(' NULL', ''); // TODO: Is this robust enough?
|
|
221
|
+
else if (delta.new.notNull === delta.old.notNull)
|
|
222
|
+
string = string.replace( delta.new.notNull ? ' NOT NULL' : ' NULL', ''); // TODO: Is this robust enough?
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
return string;
|
|
226
|
+
}
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
class DeltaRendererH2 extends DeltaRenderer {
|
|
@@ -596,7 +596,8 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
596
596
|
while (stack.length > 0) {
|
|
597
597
|
const [ current, currentRef, currentAlias ] = stack.pop();
|
|
598
598
|
if (csnUtils.isStructured(current)) {
|
|
599
|
-
|
|
599
|
+
// `cds.Map` may also be used
|
|
600
|
+
const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements || {}).reverse();
|
|
600
601
|
for (const [ name, elem ] of elements)
|
|
601
602
|
stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
|
|
602
603
|
}
|
|
@@ -485,7 +485,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
485
485
|
collector.push(translateToSourceSide(column));
|
|
486
486
|
}
|
|
487
487
|
else {
|
|
488
|
-
collector.push(
|
|
488
|
+
collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
else if (art) { // source side - with local scope
|
|
@@ -503,14 +503,15 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
503
503
|
|
|
504
504
|
|
|
505
505
|
/**
|
|
506
|
-
* Run Object.assign on all of the passed in parameters and delete a .as at the end
|
|
506
|
+
* Run Object.assign on all of the passed in parameters and delete a .as and .key at the end
|
|
507
507
|
*
|
|
508
508
|
* @param {...any} args
|
|
509
|
-
* @returns {object} The merged args without an .as property
|
|
509
|
+
* @returns {object} The merged args without an .as and .key property
|
|
510
510
|
*/
|
|
511
|
-
function
|
|
511
|
+
function assignAndDeleteAsAndKey( ...args ) {
|
|
512
512
|
const obj = Object.assign.apply(null, args);
|
|
513
513
|
delete obj.as;
|
|
514
|
+
delete obj.key;
|
|
514
515
|
return obj;
|
|
515
516
|
}
|
|
516
517
|
/**
|
|
@@ -528,19 +529,19 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
528
529
|
const column = obj._art._column;
|
|
529
530
|
if (column && column.as)
|
|
530
531
|
return translateToSourceSide(column);
|
|
531
|
-
return
|
|
532
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
|
|
532
533
|
}
|
|
533
534
|
else if (typeof obj.$env === 'string') {
|
|
534
|
-
return
|
|
535
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
|
|
535
536
|
}
|
|
536
537
|
|
|
537
|
-
return
|
|
538
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
|
|
538
539
|
}
|
|
539
540
|
else if (obj.xpr) { // we need to drill further down into .xpr
|
|
540
|
-
return
|
|
541
|
+
return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
|
|
541
542
|
}
|
|
542
543
|
else if (obj.args) {
|
|
543
|
-
return
|
|
544
|
+
return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
|
|
544
545
|
}
|
|
545
546
|
|
|
546
547
|
return obj;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { CompilerAssertion } = require('../../base/error');
|
|
4
|
+
const { forEach } = require('../../utils/objectUtils');
|
|
4
5
|
|
|
5
6
|
const directMappings = {
|
|
6
7
|
'@Common.IsDayOfCalendarMonth': replace('@Semantics.calendar.dayOfMonth'),
|
|
@@ -190,6 +191,152 @@ function remapODataAnnotations( csn ) {
|
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Do the .texts anno magic if we can be reasonably sure that we are actually dealing with a .texts entity.
|
|
196
|
+
*
|
|
197
|
+
* @param {CSN.Model} csn
|
|
198
|
+
* @param {string} artifactName
|
|
199
|
+
* @param {CSN.Artifact} artifact
|
|
200
|
+
*/
|
|
201
|
+
function sealAnnoMagicForTexts(csn, artifactName, artifact) {
|
|
202
|
+
if (artifactName.endsWith('.texts') && artifact.elements?.locale) {
|
|
203
|
+
const firstNonKey = getFirstNonKeyElement(artifact);
|
|
204
|
+
if (firstNonKey && firstNonKey.type === 'cds.String') {
|
|
205
|
+
artifact['@ObjectModel.supportedCapabilities'] ??= [];
|
|
206
|
+
if (!artifact['@ObjectModel.supportedCapabilities'].find(part => part['#'] === 'LANGUAGE_DEPENDENT_TEXT'))
|
|
207
|
+
artifact['@ObjectModel.supportedCapabilities'].push({ '#': 'LANGUAGE_DEPENDENT_TEXT' });
|
|
208
|
+
if (artifact.elements.locale['@Semantics.language'] === undefined)
|
|
209
|
+
artifact.elements.locale['@Semantics.language'] = true;
|
|
210
|
+
if (firstNonKey['@Semantics.text'] === undefined)
|
|
211
|
+
firstNonKey['@Semantics.text'] = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
* @param {CSN.Artifact} artifact
|
|
219
|
+
* @returns {CSN.Element|null}
|
|
220
|
+
*/
|
|
221
|
+
function getFirstNonKeyElement(artifact) {
|
|
222
|
+
for (const elementName in artifact.elements) {
|
|
223
|
+
if (Object.prototype.hasOwnProperty.call(artifact.elements, elementName)) {
|
|
224
|
+
if (!artifact.elements[elementName].key)
|
|
225
|
+
return artifact.elements[elementName];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
*
|
|
234
|
+
* @param {CSN.Model} csn
|
|
235
|
+
* @returns {object} Transfomer object for applyTransformations
|
|
236
|
+
*/
|
|
237
|
+
function sealAnnoMagic(csn) {
|
|
238
|
+
return {
|
|
239
|
+
'@ObjectModel.supportedCapabilities': (parent, prop, anno, path) => {
|
|
240
|
+
// Filter only for values we care about
|
|
241
|
+
const filteredAnno = anno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION' || value['#'] === 'LANGUAGE_DEPENDENT_TEXT' || value['#'] === 'ANALYTICAL_PROVIDER');
|
|
242
|
+
if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_PROVIDER').length > 0 && parent.kind === 'entity' && isPartOfINAService(csn, path[1]) && parent.elements) {
|
|
243
|
+
forEach(parent.elements, (elementName, element) => {
|
|
244
|
+
if (element.target && csn.definitions[element.target]['@ObjectModel.supportedCapabilities']?.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0) {
|
|
245
|
+
const tuples = getOnConditionAsComparisonTuples(element.on, elementName);
|
|
246
|
+
const targetEntity = csn.definitions[element.target];
|
|
247
|
+
if (element.on.length === 3 && tuples.length > 0 ) {
|
|
248
|
+
tuples.forEach(({ source }) => {
|
|
249
|
+
const sourceElement = parent.elements[source.ref[0]];
|
|
250
|
+
if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
|
|
251
|
+
sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
else if (element.on.length > 3 && tuples.length > 0 && targetEntity['@ObjectModel.representativeKey']) {
|
|
256
|
+
tuples.forEach(({ source, target }) => {
|
|
257
|
+
if (target.ref[1] === targetEntity['@ObjectModel.representativeKey']['=']) {
|
|
258
|
+
const sourceElement = parent.elements[source.ref[0]];
|
|
259
|
+
if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
|
|
260
|
+
sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0 && parent.kind === 'entity' && parent.elements) {
|
|
269
|
+
forEach(parent.elements, (_elementName, element) => {
|
|
270
|
+
if (element['@ObjectModel.text.element'] && parent.elements[element['@ObjectModel.text.element']['=']] && parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] === undefined)
|
|
271
|
+
parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] = true;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (filteredAnno.length === 1 && parent.kind && parent['@ObjectModel.modelingPattern'] === undefined) {
|
|
276
|
+
if (filteredAnno[0]['#'] === 'ANALYTICAL_PROVIDER')
|
|
277
|
+
parent['@ObjectModel.modelingPattern'] = { '#': 'ANALYTICAL_CUBE' };
|
|
278
|
+
else
|
|
279
|
+
parent['@ObjectModel.modelingPattern'] = { '#': filteredAnno[0]['#'] };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPartOfINAService(csn, artifactName) {
|
|
286
|
+
const parts = artifactName.split('.');
|
|
287
|
+
if (parts.length === 1)
|
|
288
|
+
return false; // No dots
|
|
289
|
+
for (let i = 0; i < parts.length; i++) {
|
|
290
|
+
const possibleServiceName = parts.slice(0, i).join('.');
|
|
291
|
+
const possibleDefinition = csn.definitions[possibleServiceName];
|
|
292
|
+
if (possibleDefinition?.kind === 'service')
|
|
293
|
+
return possibleDefinition['@protocol'] === 'ina';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Split the given on-condition into bite-sized tuples IF
|
|
301
|
+
* - the operator is a =
|
|
302
|
+
* - one of the arguments is of the form <assoc>.<field>
|
|
303
|
+
* - one of the arguments is of the form <field>
|
|
304
|
+
* - there are no braces
|
|
305
|
+
* - each of the comparison tuples is "joined" via "and"
|
|
306
|
+
*
|
|
307
|
+
* Return an empty array if we encounter any tuples/things that do NOT match those criteria
|
|
308
|
+
* @param {CSN.OnCondition} on
|
|
309
|
+
* @param {string} assocName
|
|
310
|
+
* @returns {object[]}
|
|
311
|
+
*/
|
|
312
|
+
function getOnConditionAsComparisonTuples(on, assocName) {
|
|
313
|
+
const validTuples = [];
|
|
314
|
+
for (let i = 0; i < on.length - 2; i += 4) {
|
|
315
|
+
let isValid = false;
|
|
316
|
+
const arg1 = on[i];
|
|
317
|
+
const operator = on[i + 1];
|
|
318
|
+
const arg2 = on[i + 2];
|
|
319
|
+
const possibleAnd = i + 3 < on.length ? on[i + 3] : 'and';
|
|
320
|
+
if (possibleAnd === 'and' && operator === '=' && (arg1.ref?.length === 1 && arg2.ref?.length === 2 && arg2.ref[0] === assocName || arg1.ref?.length === 2 && arg1.ref[0] === assocName && arg2.ref?.length === 1 )) { // TODO: Do we care about filters? Filters could cause a crash here?
|
|
321
|
+
if (arg1.ref.length === 1) { // arg1 needs to point to be <field>, arg2 needs to be <assoc>.<field>
|
|
322
|
+
validTuples.push({ source: arg1, target: arg2 });
|
|
323
|
+
isValid = true;
|
|
324
|
+
}
|
|
325
|
+
else { // arg1 needs to point to be <assoc>.<field>, arg2 needs to be <field>
|
|
326
|
+
validTuples.push({ source: arg2, target: arg1 });
|
|
327
|
+
isValid = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!isValid)
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
return validTuples;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
193
338
|
module.exports = {
|
|
194
339
|
remapODataAnnotations,
|
|
340
|
+
sealAnnoMagic,
|
|
341
|
+
sealAnnoMagicForTexts,
|
|
195
342
|
};
|
|
@@ -78,12 +78,26 @@ function effectiveCsn( model, options, messageFunctions ) {
|
|
|
78
78
|
processCalculatedElementsInEntities(csn, options);
|
|
79
79
|
associations.managedToUnmanaged(csn, options, csnUtils, messageFunctions);
|
|
80
80
|
associations.transformBacklinks(csn, options, csnUtils, messageFunctions);
|
|
81
|
-
const transformers = mergeTransformers([
|
|
82
|
-
|
|
81
|
+
const transformers = mergeTransformers([
|
|
82
|
+
options.addCdsPersistenceName ? misc.attachPersistenceName(csn, options, csnUtils) : {},
|
|
83
|
+
options.remapOdataAnnotations ? annotations.remapODataAnnotations(csn) : {},
|
|
84
|
+
misc.removeDefinitionsAndProperties(csn, options),
|
|
85
|
+
options.deriveAnalyticalAnnotations ? annotations.sealAnnoMagic(csn) : {},
|
|
86
|
+
], null);
|
|
87
|
+
|
|
88
|
+
const artifactTransformers = [];
|
|
89
|
+
if (options.deriveAnalyticalAnnotations)
|
|
90
|
+
artifactTransformers.push(annotations.sealAnnoMagicForTexts);
|
|
91
|
+
|
|
92
|
+
applyTransformations(csn, transformers, artifactTransformers, { skipIgnore: false, processAnnotations: true });
|
|
83
93
|
|
|
84
94
|
if (!options.resolveProjections)
|
|
85
95
|
redoProjections.forEach(fn => fn());
|
|
86
96
|
|
|
97
|
+
|
|
98
|
+
// Remove unapplied extensions/annotations
|
|
99
|
+
delete csn.extensions;
|
|
100
|
+
|
|
87
101
|
messageFunctions.throwWithError();
|
|
88
102
|
|
|
89
103
|
return csn;
|
|
@@ -10,6 +10,8 @@ const { forEachDefinition,
|
|
|
10
10
|
isAspect,
|
|
11
11
|
getServiceNames,
|
|
12
12
|
forEachGeneric,
|
|
13
|
+
cardinality2str,
|
|
14
|
+
getUtils
|
|
13
15
|
} = require('../model/csnUtils');
|
|
14
16
|
const { checkCSNVersion } = require('../json/csnVersion');
|
|
15
17
|
const validate = require('../checks/validator');
|
|
@@ -18,6 +20,7 @@ const expandToFinalBaseType = require('./odata/toFinalBaseType');
|
|
|
18
20
|
const { timetrace } = require('../utils/timetrace');
|
|
19
21
|
const enrichUniversalCsn = require('./universalCsn/universalCsnEnricher');
|
|
20
22
|
const flattening = require('./odata/flattening');
|
|
23
|
+
const createForeignKeyElements = require('./odata/createForeignKeys');
|
|
21
24
|
const associations = require('./db/associations')
|
|
22
25
|
const expansion = require('./db/expansion');
|
|
23
26
|
const generateDrafts = require('./draft/odata');
|
|
@@ -181,24 +184,31 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
181
184
|
// If errors are detected, throwWithAnyError() will return from further processing
|
|
182
185
|
expandStructsInExpression(csn, { skipArtifact: isExternalServiceMember, drillRef: true });
|
|
183
186
|
|
|
187
|
+
// do expansion before Fk creation because of messages reporting
|
|
184
188
|
if (!structuredOData) {
|
|
185
189
|
expansion.expandStructureReferences(csn, options, '_',
|
|
186
190
|
{ error, info, throwWithAnyError }, csnUtils,
|
|
187
191
|
{ skipArtifact: isExternalServiceMember });
|
|
188
|
-
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
createForeignKeyElements(csn, options, messageFunctions, csnUtils, { skipArtifact: isExternalServiceMember });
|
|
189
195
|
|
|
196
|
+
if (!structuredOData) {
|
|
197
|
+
const resolved = new WeakMap();
|
|
190
198
|
const { inspectRef, effectiveType } = csnRefs(csn);
|
|
199
|
+
const { getFinalTypeInfo } = getUtils(csn);
|
|
191
200
|
const { adaptRefs, transformer: refFlattener } =
|
|
192
201
|
flattening.getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, '_');
|
|
193
202
|
|
|
194
|
-
flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
|
|
195
|
-
|
|
203
|
+
const allMgdAssocDefs = flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
|
|
204
|
+
inspectRef, getFinalTypeInfo, isExternalServiceMember, error, csnUtils, options);
|
|
196
205
|
flattening.flattenAllStructStepsInRefs(csn, refFlattener, adaptRefs,
|
|
197
206
|
inspectRef, effectiveType, csnUtils, error, options,
|
|
198
207
|
{ //skip: ['action', 'aspect', 'event', 'function', 'type'],
|
|
199
208
|
skipArtifact: isExternalServiceMember,
|
|
200
209
|
});
|
|
201
|
-
|
|
210
|
+
flattening.replaceManagedAssocsAsKeys(allMgdAssocDefs, csnUtils);
|
|
211
|
+
|
|
202
212
|
// replace structured with flat dictionaries that contain
|
|
203
213
|
// rewritten path expressions
|
|
204
214
|
forEachDefinition(csn, (def) => {
|
|
@@ -223,16 +233,14 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
223
233
|
});
|
|
224
234
|
}
|
|
225
235
|
|
|
226
|
-
|
|
227
|
-
// see db/views.js::addForeignKeysToColumns
|
|
228
|
-
flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, messageFunctions, '_',
|
|
229
|
-
!structuredOData, csnUtils,{ skipArtifact: isExternalServiceMember });
|
|
236
|
+
bindCsnReferenceOnly();
|
|
230
237
|
|
|
231
238
|
// Allow using managed associations as steps in on-conditions to access their fks
|
|
232
239
|
// To be done after handleManagedAssociationsAndCreateForeignKeys,
|
|
233
240
|
// since then the foreign keys of the managed assocs are part of the elements
|
|
234
|
-
if(!structuredOData)
|
|
241
|
+
if(!structuredOData) {
|
|
235
242
|
forEachDefinition(csn, associations.getFKAccessFinalizer(csn, csnUtils, '_'));
|
|
243
|
+
}
|
|
236
244
|
|
|
237
245
|
// structure flattener reports errors, further processing is not safe -> throw exception in case of errors
|
|
238
246
|
throwWithAnyError();
|
|
@@ -277,7 +285,8 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
277
285
|
!(propertyName === 'enum' || propertyName === 'returns') &&
|
|
278
286
|
(!member.virtual || def.query)) {
|
|
279
287
|
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
|
|
280
|
-
member['@cds.persistence.name'] = getElementDatabaseNameOf(member.$defPath?.slice(1).join('.')
|
|
288
|
+
member['@cds.persistence.name'] = getElementDatabaseNameOf((!member['@odata.foreignKey4'] && member.$defPath?.slice(1).join('.'))
|
|
289
|
+
|| memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
// Mark fields with @odata.on.insert/update as @Core.Computed
|
|
@@ -286,6 +295,12 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
286
295
|
// Resolve annotation shorthands for elements, actions, action parameters
|
|
287
296
|
renameShorthandAnnotations(member);
|
|
288
297
|
|
|
298
|
+
// If an association was modelled as not null, like so:
|
|
299
|
+
// <associationName>: Association to <target> not null;
|
|
300
|
+
// a cardinality property is set to the association member
|
|
301
|
+
// with the value { "min": 1 };
|
|
302
|
+
setCardinalityToNotNullAssociations(member);
|
|
303
|
+
|
|
289
304
|
// - If the association target is annotated with @cds.odata.valuelist, annotate the
|
|
290
305
|
// association with @Common.ValueList.viaAssociation
|
|
291
306
|
// - Check for @Analytics.Measure and @Aggregation.default
|
|
@@ -419,6 +434,27 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
|
|
437
|
+
// If an association was modelled as not null, like so:
|
|
438
|
+
// <associationName>: Association to <target> not null;
|
|
439
|
+
// a cardinality property is set to the association member
|
|
440
|
+
// with the value { "min": 1 };
|
|
441
|
+
function setCardinalityToNotNullAssociations(member) {
|
|
442
|
+
if (member.target && member.keys && !member.on) {
|
|
443
|
+
if (member.notNull) {
|
|
444
|
+
if (member.cardinality === undefined)
|
|
445
|
+
member.cardinality = {};
|
|
446
|
+
// min=0 is falsy => check for undefined
|
|
447
|
+
if (member.cardinality.min === undefined) {
|
|
448
|
+
member.cardinality.min = 1;
|
|
449
|
+
}
|
|
450
|
+
else if (member.cardinality.min === 0) {
|
|
451
|
+
warning(null, member.$path, { value: cardinality2str(member, false), code: 'not null' },
|
|
452
|
+
'Expected target cardinality $(VALUE) and $(CODE) to match');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
422
458
|
// Apply default type facets to each type definition and every member
|
|
423
459
|
// But do not apply default string length (as in DB)
|
|
424
460
|
function setDefaultTypeFacets(def) {
|
|
@@ -466,4 +502,11 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
466
502
|
}
|
|
467
503
|
}
|
|
468
504
|
|
|
505
|
+
function bindCsnReferenceOnly() {
|
|
506
|
+
// invalidate caches for CSN ref API
|
|
507
|
+
const csnRefApi = csnRefs(csn);
|
|
508
|
+
Object.assign(csnUtils, csnRefApi);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
469
512
|
} // transform4odataWithCsn
|
|
@@ -856,6 +856,13 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
856
856
|
checkTypeParamValue(node, 'srid', { max: Number.MAX_SAFE_INTEGER }, path);
|
|
857
857
|
break;
|
|
858
858
|
}
|
|
859
|
+
case 'cds.Map': {
|
|
860
|
+
if (options.sqlDialect === 'plain')
|
|
861
|
+
error('ref-unsupported-type', path, { '#': 'dialect', type: node.type, value: 'plain' });
|
|
862
|
+
else if (options.transformation === 'hdbcds')
|
|
863
|
+
error('ref-unsupported-type', path, {'#': 'hdbcds', type: node.type, value: options.sqlDialect });
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
859
866
|
case 'cds.Vector': {
|
|
860
867
|
if (options.sqlDialect !== 'hana') {
|
|
861
868
|
error('ref-unsupported-type', path, {
|