@sap/cds-compiler 3.3.2 → 3.4.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/cdsc.js +3 -1
  3. package/doc/CHANGELOG_BETA.md +17 -0
  4. package/lib/api/main.js +147 -18
  5. package/lib/api/validate.js +8 -3
  6. package/lib/base/dictionaries.js +6 -6
  7. package/lib/base/keywords.js +104 -0
  8. package/lib/base/message-registry.js +136 -67
  9. package/lib/base/messages.js +59 -48
  10. package/lib/base/model.js +1 -0
  11. package/lib/checks/actionsFunctions.js +1 -1
  12. package/lib/checks/cdsPersistence.js +1 -1
  13. package/lib/checks/checkForTypes.js +13 -8
  14. package/lib/checks/defaultValues.js +3 -1
  15. package/lib/checks/elements.js +1 -1
  16. package/lib/checks/parameters.js +4 -2
  17. package/lib/checks/queryNoDbArtifacts.js +1 -1
  18. package/lib/checks/sql-snippets.js +12 -10
  19. package/lib/checks/validator.js +14 -4
  20. package/lib/compiler/assert-consistency.js +8 -7
  21. package/lib/compiler/checks.js +30 -20
  22. package/lib/compiler/define.js +89 -25
  23. package/lib/compiler/extend.js +21 -18
  24. package/lib/compiler/finalize-parse-cdl.js +14 -9
  25. package/lib/compiler/populate.js +30 -8
  26. package/lib/compiler/propagator.js +4 -2
  27. package/lib/compiler/resolve.js +11 -5
  28. package/lib/compiler/shared.js +66 -48
  29. package/lib/compiler/tweak-assocs.js +2 -3
  30. package/lib/compiler/utils.js +11 -0
  31. package/lib/edm/annotations/genericTranslation.js +7 -4
  32. package/lib/edm/csn2edm.js +1 -1
  33. package/lib/gen/language.checksum +1 -1
  34. package/lib/gen/language.interp +1 -1
  35. package/lib/gen/languageParser.js +3565 -3544
  36. package/lib/json/csnVersion.js +13 -13
  37. package/lib/json/from-csn.js +140 -158
  38. package/lib/json/to-csn.js +23 -5
  39. package/lib/language/.eslintrc.json +4 -0
  40. package/lib/language/antlrParser.js +7 -10
  41. package/lib/language/docCommentParser.js +1 -2
  42. package/lib/language/errorStrategy.js +54 -27
  43. package/lib/language/genericAntlrParser.js +115 -84
  44. package/lib/language/language.g4 +29 -25
  45. package/lib/language/multiLineStringParser.js +75 -63
  46. package/lib/main.js +1 -0
  47. package/lib/model/csnRefs.js +4 -3
  48. package/lib/model/csnUtils.js +39 -7
  49. package/lib/model/sortViews.js +7 -3
  50. package/lib/modelCompare/compare.js +49 -15
  51. package/lib/modelCompare/filter.js +83 -0
  52. package/lib/optionProcessor.js +5 -1
  53. package/lib/render/manageConstraints.js +9 -5
  54. package/lib/render/toCdl.js +120 -62
  55. package/lib/render/toHdbcds.js +1 -1
  56. package/lib/render/toSql.js +6 -2
  57. package/lib/render/utils/common.js +7 -0
  58. package/lib/sql-identifier.js +7 -0
  59. package/lib/transform/db/assertUnique.js +27 -38
  60. package/lib/transform/db/expansion.js +11 -4
  61. package/lib/transform/db/temporal.js +3 -1
  62. package/lib/transform/db/transformExists.js +7 -1
  63. package/lib/transform/db/views.js +42 -13
  64. package/lib/transform/draft/db.js +2 -2
  65. package/lib/transform/forRelationalDB.js +12 -6
  66. package/lib/transform/localized.js +1 -1
  67. package/lib/transform/odata/typesExposure.js +2 -1
  68. package/lib/transform/parseExpr.js +245 -0
  69. package/lib/transform/transformUtilsNew.js +23 -14
  70. package/lib/transform/translateAssocsToJoins.js +12 -12
  71. package/lib/utils/term.js +5 -5
  72. package/package.json +2 -2
  73. package/share/messages/message-explanations.json +1 -1
  74. package/share/messages/{syntax-expected-integer.md → syntax-expecting-integer.md} +1 -1
@@ -3,6 +3,7 @@
3
3
  const { forEachDefinition, hasAnnotationValue } = require('../../model/csnUtils');
4
4
  const { getTransformers } = require('../transformUtilsNew');
5
5
  const { setProp } = require('../../base/model');
6
+ const { pathName } = require('../../compiler/utils');
6
7
 
7
8
 
8
9
  /**
@@ -38,13 +39,14 @@ function processAssertUnique(csn, options, error, info) {
38
39
  // filter unique constraints from annotations
39
40
  for (const propName in artifact) {
40
41
  if (propName.startsWith('@assert.unique') && artifact[propName] !== null) {
42
+ const anno = propName;
41
43
  // Constraint Name check
42
44
  const constraintName = propName.split('.').splice(2);
43
45
  if (constraintName.length === 0)
44
- err(propName, 'Table constraint cannot be anonymous');
46
+ error(null, [ 'definitions', artifactName ], { anno }, '$(ANNO): Table constraint can\'t be anonymous');
45
47
  if (constraintName.length > 1)
46
48
  // Neither HANA CDS nor HANA SQL allow dots in index names
47
- err(propName, "Illegal character '.' in constraint name");
49
+ error(null, [ 'definitions', artifactName ], { anno }, '$(ANNO): Illegal character \'.\' in constraint name');
48
50
 
49
51
  const propValue = artifact[propName];
50
52
  // Constraint value check, returns array of path values
@@ -79,10 +81,12 @@ function processAssertUnique(csn, options, error, info) {
79
81
  pathxrefs[pstr]++;
80
82
  });
81
83
  Object.keys(pathxrefs).forEach((k) => {
82
- if (pathxrefs[k] > 1)
83
- err(propName, `Final path "${k}" can only be specified once`);
84
+ if (pathxrefs[k] > 1) {
85
+ error(null, [ 'definitions', artifactName ], { anno, id: k },
86
+ '$(ANNO): Final path $(ID) can only be specified once');
87
+ }
84
88
  });
85
- // 9) Add into constraint cross reference
89
+ // 9) Add into constraint cross-reference
86
90
  if (constraintKey.length) {
87
91
  if (constraintXrefs[constraintKey])
88
92
  constraintXrefs[constraintKey].push(propName);
@@ -99,7 +103,7 @@ function processAssertUnique(csn, options, error, info) {
99
103
  for (const key in constraintXrefs) {
100
104
  const val = constraintXrefs[key];
101
105
  if (val.length > 1)
102
- err(val.join(', '), 'Constraint can only be specified once');
106
+ error(null, [ 'definitions', artifactName ], { annos: val }, '$(ANNOS): Constraint can only be specified once');
103
107
  }
104
108
  // preserve dictionary in '$tableConstraints' on the artifact for path rewriting and rendering
105
109
  if (Object.keys(constraintDict).length) {
@@ -119,17 +123,18 @@ function processAssertUnique(csn, options, error, info) {
119
123
  */
120
124
  function checkVal(val, propName) {
121
125
  const paths = [];
126
+ const loc = [ 'definitions', artifactName ];
122
127
  if (!Array.isArray(val)) {
123
- err(propName, `Value '${JSON.stringify(unref(val))}' is not an array`);
128
+ error(null, loc, { anno: propName, value: JSON.stringify(unref(val)) }, '$(ANNO): Value $(VALUE) is not an array');
124
129
  }
125
130
  else {
126
131
  if (val.length === 0)
127
- inf(propName, 'Empty annotation is ignored');
132
+ info(null, loc, { anno: propName }, '$(ANNO): Empty annotation is ignored');
128
133
 
129
134
  val.forEach((v) => {
130
135
  const p = v['='];
131
136
  if (!p)
132
- err(propName, `Value '${JSON.stringify(unref(v))}' is not a path`);
137
+ error(null, loc, { anno: propName, value: JSON.stringify(unref(v)) }, '$(ANNO): Value $(VALUE) is not a path');
133
138
  else
134
139
  paths.push(p);
135
140
  });
@@ -185,61 +190,45 @@ function processAssertUnique(csn, options, error, info) {
185
190
  return;
186
191
  path.isChecked = true;
187
192
  let foundErr = false;
193
+ const name = pathName(path.ref);
194
+ const loc = [ 'definitions', artifactName ];
188
195
  for (let i = 0; i < path.ref.length && !foundErr; i++) {
189
196
  const art = path.ref[i]._art;
197
+ const elemref = path.ref[i].id;
190
198
  if (art) {
191
199
  if (art.items) {
192
- msg(`'Array of/many' element "${path.ref[i].id}" is not allowed`);
200
+ error(null, loc, { elemref, name, anno: constraintName },
201
+ '$(ANNO): \'Array of/many\' element $(ELEMREF) is not allowed in $(NAME)');
193
202
  delete path._art;
194
203
  foundErr = true;
195
204
  }
196
205
  if (art.target) {
197
206
  if (art.on) {
198
- msg(`Unmanaged association "${path.ref[i].id}" is not allowed`);
207
+ error(null, loc, { elemref, name, anno: constraintName },
208
+ '$(ANNO): Unmanaged association $(ELEMREF) is not allowed in $(NAME)');
199
209
  delete path._art;
200
210
  foundErr = true;
201
211
  }
202
212
  if (art.keys && i < path.ref.length - 1) {
203
- msg(`Element access via managed association "${path.ref[i].id}" is not allowed`);
213
+ error(null, loc, { elemref, name, anno: constraintName },
214
+ '$(ANNO): Element access via managed association $(ELEMREF) is not allowed in $(NAME)');
204
215
  delete path._art;
205
216
  foundErr = true;
206
217
  }
207
218
  }
208
219
  }
209
220
  else {
210
- err(constraintName, `"${path.ref[i].id}" has not been found`);
221
+ error(null, loc, { elemref, anno: constraintName }, '$(ANNO): $(ELEMREF) has not been found');
211
222
  foundErr = true;
212
223
  }
213
224
  }
214
225
 
215
226
  if (!foundErr && path._art && [ 'cds.LargeBinary', 'cds.LargeString',
216
- 'cds.hana.CLOB', 'cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY' ].includes(path._art.type))
217
- msg(`"Type ${path._art.type}" not allowed`);
218
-
219
-
220
- /**
221
- * @param {string} message
222
- */
223
- function msg(message) {
224
- err(constraintName, `${message} in "${path.ref.map(p => p.id).join('.')}"`);
227
+ 'cds.hana.CLOB', 'cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY' ].includes(path._art.type)) {
228
+ error(null, loc, { type: path._art.type, name, anno: constraintName },
229
+ '$(ANNO): Type $(TYPE) not allowed in $(NAME)');
225
230
  }
226
231
  }
227
-
228
- // message macros for unified messaging
229
- /**
230
- * @param {string} propName
231
- * @param {string} message
232
- */
233
- function err(propName, message) {
234
- error(null, [ 'definitions', artifactName ], `${propName}: ${message}`);
235
- }
236
- /**
237
- * @param {string} propName
238
- * @param {string} message
239
- */
240
- function inf(propName, message) {
241
- info(null, [ 'definitions', artifactName ], `${propName}: ${message}`);
242
- }
243
232
  }
244
233
  }
245
234
 
@@ -62,7 +62,8 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
62
62
  * For such skipped things, error for usage of assoc pointing to them and and ignore publishing of assoc pointing to them.
63
63
  */
64
64
  function rewriteExpandInline() {
65
- const { cleanup, _dependents } = setDependencies(csn);
65
+ let cleanup;
66
+ let _dependents;
66
67
 
67
68
  const entity = findAnEntity();
68
69
  const toDummyfy = [];
@@ -76,7 +77,6 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
76
77
  const root = get$combined({ SELECT: parent });
77
78
  if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
78
79
  const rewritten = rewrite(root, parent.columns, parent.excluding);
79
- parent.columns = rewritten.columns;
80
80
  /*
81
81
  * Do not remove unexpandable many columns in OData
82
82
  */
@@ -96,7 +96,8 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
96
96
  if (!options.toOdata)
97
97
  dummyfy();
98
98
 
99
- cleanup.forEach(fn => fn());
99
+ if (cleanup)
100
+ cleanup.forEach(fn => fn());
100
101
 
101
102
  ({ effectiveType, inspectRef } = csnRefs(csn));
102
103
 
@@ -104,7 +105,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
104
105
  const publishing = [];
105
106
  // OData must allow navigations to @cds.persistence.skip targets
106
107
  // as valid navigations in the API
107
- if (!options.toOdata) {
108
+ if (options.transformation !== 'odata') {
108
109
  applyTransformations(csn, {
109
110
  target: (parent, name, target, path) => {
110
111
  if (toDummyfy.indexOf(target) !== -1) {
@@ -207,6 +208,9 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
207
208
  * @param {string} name
208
209
  */
209
210
  function markAsToDummyfy(artifact, name) {
211
+ if (!_dependents && !cleanup)
212
+ ({ cleanup, _dependents } = setDependencies(csn, csnUtils));
213
+
210
214
  const stack = [ [ artifact, name ] ];
211
215
  while (stack.length > 0) {
212
216
  const [ a, n ] = stack.pop();
@@ -355,6 +359,9 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
355
359
  else if (current.val !== undefined || current.func !== undefined) {
356
360
  expanded.push(Object.assign(current, { as: currentAlias.join(pathDelimiter) }));
357
361
  }
362
+ else if (current.$scope === '$magic' || current.$scope === '$self') {
363
+ expanded.push(Object.assign({}, current, { as: currentAlias.join(pathDelimiter) } ));
364
+ }
358
365
  else { // preserve stuff like .cast for redirection
359
366
  expanded.push(Object.assign({}, current, { ref: currentRef, as: currentAlias.join(pathDelimiter) } ));
360
367
  }
@@ -81,7 +81,9 @@ function getViewDecorator(csn, messageFunctions) {
81
81
  }
82
82
  }
83
83
  else {
84
- info(null, [ 'definitions', artifactName ], `No temporal WHERE clause added as "${from[0].errorParent}"."${from[0].name}" and "${to[0].errorParent}"."${to[0].name}" are not of same origin`);
84
+ info(null, [ 'definitions', artifactName ],
85
+ { source: `${from[0].errorParent}.${from[0].name}`, target: `${to[0].errorParent}.${to[0].name}` },
86
+ 'No temporal WHERE clause added as $(SOURCE) and $(TARGET) are not of same origin');
85
87
  }
86
88
  }
87
89
  else if (from.length > 0 || to.length > 0) {
@@ -428,6 +428,11 @@ function handleExists(csn, options, error) {
428
428
  * @returns {object[]} The stuff to add to the where
429
429
  */
430
430
  function translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
431
+ if (current.$scope === '$self') {
432
+ error('ref-unexpected-exists-self', current.$path, { id: current.ref[0], name: 'exists' }, 'With $(NAME), path steps must not start with $(ID)');
433
+ return [];
434
+ }
435
+
431
436
  const whereExtension = [];
432
437
  for (let j = 0; j < root.keys.length; j++) {
433
438
  const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
@@ -800,7 +805,8 @@ function handleExists(csn, options, error) {
800
805
  }
801
806
  else if (part.$scope === '$self') { // source side - "absolute" scope
802
807
  // Same message as in forRelationalDB/transformDollarSelfComparisonWithUnmanagedAssoc
803
- error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
808
+ error(null, part.$path, { name: '$self' },
809
+ 'An association that uses $(NAME) in its ON-condition can\'t be compared to "$self"');
804
810
  }
805
811
  else if (partInspect.art) { // source side - with local scope
806
812
  where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
@@ -247,14 +247,14 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
247
247
  if (isUnion(queryPath) && options.transformation === 'hdbcds') {
248
248
  if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
249
249
  if (elem.keys)
250
- info(null, queryPath, `Managed association "${elemName}", published in a UNION, will be ignored`);
250
+ info(null, queryPath, { name: elemName }, 'Managed association $(NAME), published in a UNION, will be ignored');
251
251
  else
252
- info(null, queryPath, `Association "${elemName}", published in a UNION, will be ignored`);
252
+ info(null, queryPath, { name: elemName }, 'Association $(NAME), published in a UNION, will be ignored');
253
253
 
254
254
  elem._ignore = true;
255
255
  }
256
256
  else {
257
- error(null, queryPath, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
257
+ error(null, queryPath, { name: elemName }, 'Association $(NAME) can\'t be published in a SAP HANA CDS UNION');
258
258
  }
259
259
  }
260
260
  else if (queryPath.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
@@ -325,17 +325,40 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
325
325
  * due to an issue with HANA - only for hdbcds-hdbcds, I assume flattening
326
326
  * takes care of this for the other cases already
327
327
  *
328
- * @param {CSN.Query} query
328
+ * @param {CSN.Column} col
329
329
  * @param {CSN.Path} path
330
330
  */
331
- function addImplicitAliasWithAssoc(query, path) {
332
- for (let i = 0; i < query.SELECT.columns.length; i++) {
333
- const col = query.SELECT.columns[i];
334
- if (!col.as && col.ref && col.ref.length > 1) {
335
- const { links } = inspectRef(path.concat([ 'columns', i ]));
336
- if (links && links.slice(0, -1).some(({ art }) => isAssocOrComposition(art && art.type || '')))
337
- col.as = getLastRefStepString(col.ref);
338
- }
331
+ function addImplicitAliasWithAssoc(col, path) {
332
+ if (!col.as && col.ref && col.ref.length > 1) {
333
+ const { links } = inspectRef(path);
334
+ if (links && links.slice(0, -1).some(({ art }) => isAssocOrComposition(art && art.type || '')))
335
+ col.as = getLastRefStepString(col.ref);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * If simply selecting from a param like `:param`, we need to add an implicit alias like `:param as param`
341
+ * due to an issue with HANA
342
+ *
343
+ * @param {CSN.Column} col
344
+ */
345
+ function addImplicitAliasWithLonelyParam(col) {
346
+ if (!col.as && col.param)
347
+ col.as = getLastRefStepString(col.ref);
348
+ }
349
+
350
+
351
+ /**
352
+ * Loop over the columns and call all of the given functions with the column and the path
353
+ *
354
+ * @param {Function[]} functions
355
+ * @param {CSN.Column[]} columns
356
+ * @param {CSN.Path} path
357
+ */
358
+ function processColumns(functions, columns, path) {
359
+ for (let i = 0; i < columns.length; i++) {
360
+ const col = columns[i];
361
+ functions.forEach(fn => fn(col, path.concat(i)));
339
362
  }
340
363
  }
341
364
 
@@ -401,8 +424,14 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
401
424
  query.SELECT.columns = Object.keys(elements).filter(elem => !elements[elem]._ignore).map(key => stripLeadingSelf(columnMap[key]));
402
425
  // If following an association, explicitly set the implicit alias
403
426
  // due to an issue with HANA - this seems to only have an effect on ref files with hdbcds-hdbcds, so only run then
427
+ const columnProcessors = [];
428
+ if (options.transformation === 'hdbcds' || options.transformation === 'sql' && options.sqlDialect === 'hana')
429
+ columnProcessors.push(addImplicitAliasWithLonelyParam);
404
430
  if (options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds')
405
- addImplicitAliasWithAssoc(query, path);
431
+ columnProcessors.push(addImplicitAliasWithAssoc);
432
+
433
+ if (columnProcessors.length > 0)
434
+ processColumns(columnProcessors, query.SELECT.columns, path.concat('columns'));
406
435
 
407
436
  delete query.SELECT.excluding; // just to make the output of the new transformer the same as the old
408
437
 
@@ -165,8 +165,8 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
165
165
  }
166
166
 
167
167
  else {
168
- error(null, [ 'definitions', draftRootName ], { name: persistenceName },
169
- `Entity $(NAME) already generated by draft root "${definingDraftRoot}"`);
168
+ error(null, [ 'definitions', draftRootName ], { name: persistenceName, alias: definingDraftRoot },
169
+ 'Entity $(NAME) already generated by draft root $(ALIAS)');
170
170
  }
171
171
 
172
172
  return;
@@ -310,7 +310,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
310
310
  * For to.hdbcds with naming mode "hdbcds", no foreign keys are calculated,
311
311
  * hence we do not generate the referential constraints for them.
312
312
  */
313
- if(options.sqlDialect !== 'plain' && doA2J)
313
+ if(options.sqlDialect !== 'plain' && options.sqlDialect !== 'h2' && doA2J)
314
314
  createReferentialConstraints(csn, options);
315
315
 
316
316
  // no constraints for drafts
@@ -625,12 +625,12 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
625
625
  }
626
626
  else if (options.sqlDialect === 'sqlite') { // view with params
627
627
  // Allow with plain
628
- error(null, [ 'definitions', artifactName ], `SQLite does not support entities with parameters`);
628
+ error(null, [ 'definitions', artifactName ], 'SQLite does not support entities with parameters');
629
629
  }
630
630
  else {
631
631
  for (const pname in artifact.params) {
632
632
  if (pname.match(/\W/g) || pname.match(/^\d/) || pname.match(/^_/)) { // parameter name must be regular SQL identifier
633
- warning(null, [ 'definitions', artifactName, 'params', pname ], `Expecting regular SQL-Identifier`);
633
+ warning(null, [ 'definitions', artifactName, 'params', pname ], 'Expecting regular SQL-Identifier');
634
634
  }
635
635
  else if (options.sqlMapping !== 'plain' && pname.toUpperCase() !== pname) { // not plain mode: param name must be all upper
636
636
  warning(null, [ 'definitions', artifactName, 'params', pname ], { name: options.sqlMapping },
@@ -704,6 +704,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
704
704
  node.length = 36;
705
705
  setProp(node, '$renamed', 'cds.UUID');
706
706
  }
707
+
708
+ if(options.sqlDialect === 'h2' && val === 'cds.Decimal' && !node.scale) {
709
+ node[key] = 'cds.DecimalFloat'; // cds.Decimal and cds.Decimal(p) should map do DECFLOAT for h2
710
+ }
711
+
707
712
  // Length/Precision/Scale is done in addDefaultTypeFacets
708
713
  }
709
714
 
@@ -860,7 +865,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
860
865
  // not to the view).
861
866
  // FIXME: This also means that corresponding key fields should be in the select list etc ...
862
867
  if (!art.query && !art.projection && assoc.target && assoc.target != artifactName)
863
- error(null, path, `Only an association that points back to this artifact can be compared to "$self"`);
868
+ error(null, path, { name: '$self' }, 'Only an association that points back to this artifact can be compared to $(NAME)');
864
869
 
865
870
 
866
871
  // Check: The forward link <assocOp> must not contain '$self' in its own ON-condition
@@ -868,7 +873,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
868
873
  const containsDollarSelf = assoc.on.some(isDollarSelfOrProjectionOperand);
869
874
 
870
875
  if (containsDollarSelf)
871
- error(null, path, `An association that uses "$self" in its ON-condition can't be compared to "$self"`);
876
+ error(null, path, { name: '$self' },
877
+ 'An association that uses $(NAME) in its ON-condition can\'t be compared to $(NAME)');
872
878
  }
873
879
 
874
880
  // Transform comparison of $self to managed association into AND-combined foreign key comparisons
@@ -1086,7 +1092,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
1086
1092
  else if (art.elements) {
1087
1093
  // The reference is structured
1088
1094
  if (isFulltextIndex)
1089
- error(null, path, `"${ artName }": A fulltext index can't be defined on a structured element`);
1095
+ error(null, path, { name: artName }, 'A fulltext index can\'t be defined on a structured element $(NAME)');
1090
1096
  // First, compute the name from the path, e.g ['s', 's1', 's2' ] will result in 'S_s1_s2' ...
1091
1097
  const refPath = flattenStructStepsInRef(val.ref, path);
1092
1098
  // ... and take this as the prefix for all elements
@@ -73,7 +73,7 @@ const _targetFor = Symbol('_targetFor');
73
73
  */
74
74
  function _addLocalizationViews(csn, options, useJoins, config) {
75
75
  // Don't try to create convenience views with errors.
76
- if (hasErrors(options.messages))
76
+ if (hasErrors(options.messages)) // TODO: this is actually wrong, consider --test-mode
77
77
  return csn;
78
78
 
79
79
  const messageFunctions = makeMessageFunction(csn, options);
@@ -108,7 +108,8 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
108
108
  if (newType) {
109
109
  // error, if it was not exposed by us
110
110
  if (!exposedTypes[fullQualifiedNewTypeName]) {
111
- error(null, path, `Can't create artificial type "${fullQualifiedNewTypeName}" for "${memberName}" because the name is already used`);
111
+ error(null, path, { type: fullQualifiedNewTypeName, name: memberName },
112
+ 'Can\'t create artificial type $(TYPE) for $(NAME) because the name is already used');
112
113
  return;
113
114
  }
114
115
  }
@@ -0,0 +1,245 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * parseExpr accepts any JSON object and tries to convert a token stream expression
5
+ * array into an AST like expression with CDL operator precedence.
6
+ *
7
+ * The following operators are supported:
8
+ *
9
+ * Multiplication/Division: '*', '/'
10
+ * Addition/Subtraction: '+', '-'
11
+ * Concatenation: '||'
12
+ * Relational: '=', '<>', '>', '>=', '<', '<=', '!=', 'like', 'in', 'exists', 'between and'
13
+ * Unary: 'is [not] null', 'not'
14
+ * Conditional: 'case [when then]+ [else]? end', 'and', 'or'
15
+ *
16
+ * Not yet implmemented: 'new'
17
+ *
18
+ * This is not an optimized LL(1) parser but a token 'sniffer'. A stream is
19
+ * cracked up in sub streams and passed down to the next higher function.
20
+ *
21
+ * Complex aggregates like case/when/else/end and between are parsed first to pass down the
22
+ * resulting sub expressions and avoiding 'and' ambiguities.
23
+ *
24
+ * Sub expressions are grouped as arrays, the final AST is an array of nested arrays.
25
+ * Alternatively, an object like AST can be produced by setting argument 'array' to false.
26
+ *
27
+ * This parser intentionally does no error handling. If a clause is malformed, it is accepted as is.
28
+ *
29
+ * @param {any} xpr A JSON object.
30
+ * @param {Boolean} array Bias AST representation.
31
+ */
32
+
33
+ function parseExpr(xpr, array=true) {
34
+ return parseExprInt(xpr);
35
+
36
+ function parseExprInt(xpr) {
37
+ return conditionOR(...CaseWhen(xpr));
38
+ }
39
+
40
+ function CaseWhen(xpr) {
41
+ if(Array.isArray(xpr))
42
+ inner(xpr);
43
+ return [xpr, 0, Array.isArray(xpr) ? xpr.length : 1];
44
+
45
+ // replace case/end from inner to outer
46
+ function inner(pxpr, lvl=0) {
47
+ const s = pxpr.findIndex(t => t === 'case');
48
+ if(s >= 0) {
49
+ let e = findLastIndex(pxpr, 'end');
50
+ pxpr = pxpr.slice(s+1, e);
51
+ const dist = inner(pxpr, lvl+1);
52
+ e -= dist;
53
+ if(dist > 0)
54
+ pxpr = xpr.slice(s+1, e+1);
55
+ const caseTree = array ? [ 'case' ] : { 'case': [] };
56
+ let i = pxpr.findIndex(t => t === 'else');
57
+ let elseCond = undefined;
58
+ if(i >= 0) {
59
+ elseCond = pxpr.slice(i+1);
60
+ pxpr = pxpr.slice(0, i);
61
+ }
62
+ i = pxpr.findIndex(t => t === 'when');
63
+ while(i >= 0) {
64
+ pxpr = pxpr.slice(i+1);
65
+ const when = { 'when': [] };
66
+ if(array)
67
+ caseTree.push('when');
68
+ else
69
+ caseTree.case.push(when);
70
+ i = pxpr.findIndex(t => t === 'then');
71
+ if(i >= 0) {
72
+ const arg = pxpr.slice(0, i);
73
+ if(array)
74
+ caseTree.push(arg);
75
+ else
76
+ when.when.push(arg.length === 1 ? arg[0] : arg);
77
+ }
78
+ pxpr = pxpr.slice(i+1);
79
+ i = pxpr.findIndex(t => t === 'when');
80
+ const arg = ((i >= 0) ? pxpr.slice(0, i) : pxpr);
81
+ if(array)
82
+ caseTree.push('then', arg);
83
+ else
84
+ when.when.push(arg.length === 1 ? arg[0] : arg);
85
+ }
86
+ if(elseCond) {
87
+ if(array)
88
+ caseTree.push('else', elseCond);
89
+ else
90
+ caseTree.case.push(elseCond.length === 1 ? elseCond[0] : elseCond);
91
+ }
92
+ if(array)
93
+ caseTree.push('end');
94
+ if(lvl > 0)
95
+ xpr.splice(s+1, e-s+1, caseTree);
96
+ else {
97
+ xpr = caseTree;
98
+ }
99
+ return e-s+1;
100
+ }
101
+ else
102
+ return 0;
103
+ }
104
+
105
+ function findLastIndex(expr, token, l=expr.length-1) {
106
+ while(l >= 0 && expr[l] !== token) l--;
107
+ return l;
108
+ }
109
+ }
110
+
111
+ function conditionOR(xpr, s, e) {
112
+ return binaryExpr(xpr, ['or'], conditionAnd, s, e);
113
+ }
114
+ function conditionAnd(xpr, s, e) {
115
+ return binaryExpr(xpr, (xpr, s, e) => {
116
+ let a = s-1;
117
+ let b;
118
+ // scan for 'and', skip 'between/and'
119
+ do {
120
+ b = false;
121
+ for(a++; xpr[a] !== 'and' && a < e; a++) {
122
+ if(xpr[a] === 'between')
123
+ b = true;
124
+ }
125
+ } while(b && a < e)
126
+
127
+ if(!b && a < e)
128
+ return [1, a]
129
+ else
130
+ return [1, -1];
131
+ }, conditionTerm, s, e);
132
+ }
133
+
134
+ function conditionTerm(xpr, s, e) {
135
+ if(Array.isArray(xpr)) {
136
+ if(xpr.length >= 3 && xpr[s+1] === 'is') {
137
+ if(xpr[s+2] === 'null')
138
+ return array ? [ conditionOR(xpr[s]), 'is', 'null' ] : { 'isNull': conditionOR(xpr[s]) };
139
+ else if(xpr[s+2] === 'not' && xpr[s+3] === 'null')
140
+ return array ? [ conditionOR(xpr[s]), 'is', 'not', 'null' ] : { 'isNotNull': conditionOR(xpr[s]) };
141
+ }
142
+ if(xpr[s] === 'not')
143
+ return array ? [ 'not', conditionTerm(xpr, s+1, e) ] : { 'not': conditionTerm(xpr, s+1, e) };
144
+ if(xpr[s] === 'exists')
145
+ return array ? [ 'exists', conditionOR(xpr[s+1]) ] : { 'exists': conditionOR(xpr[s+1]) };
146
+ }
147
+ return compareTerm(xpr, s, e);
148
+ }
149
+
150
+ function compareTerm(xpr, s, e) {
151
+ if(Array.isArray(xpr)) {
152
+ let i = s;
153
+ while(i < e && xpr[i] !== 'between') i++;
154
+ const b = i < e ? i : -1;
155
+ while(i < e && xpr[i] !== 'and') i++;
156
+ const a = i < e ? i : -1;
157
+ if(b >= 0) {
158
+ const expr = expression(xpr, s, b);
159
+ const between = array ? [ expr, 'between' ] : { 'between': [ expr ] };
160
+ if(a >= 0) {
161
+ const lower = expression(xpr, b+1, a);
162
+ const upper = expression(xpr, a+1, e);
163
+ if(array)
164
+ between.push(lower, 'and', upper);
165
+ else {
166
+ between.between.push(lower, upper);
167
+ }
168
+ }
169
+ else {
170
+ const unspec = expression(xpr, b+1, e);
171
+ if(array)
172
+ between.push(unspec);
173
+ else
174
+ between.between.push(unspec);
175
+ }
176
+ return between;
177
+ }
178
+ }
179
+ return binaryExpr(xpr, ['=', '<>', '>', '>=', '<', '<=', '!=', 'like', 'in'], expression, s, e);
180
+ }
181
+
182
+ function expression(xpr, s, e) {
183
+ return binaryExpr(xpr, ['||'], exprAddSub, s, e);
184
+ }
185
+
186
+ function exprAddSub(xpr, s, e) {
187
+ return binaryExpr(xpr, ['+', '-'], exprMulDiv, s, e);
188
+ }
189
+
190
+ function exprMulDiv(xpr, s, e) {
191
+ return binaryExpr(xpr, ['*', '/'], terminal, s, e);
192
+ }
193
+
194
+ function terminal(xpr, s, e) {
195
+ if(Array.isArray(xpr) && xpr.length > 0) {
196
+ if(e-s <= 1)
197
+ return parseExprInt(xpr[e-1]);
198
+ else
199
+ return xpr.slice(s, e).map(parseExprInt);
200
+ }
201
+ if (typeof xpr === 'object') {
202
+ for(let n in xpr) {
203
+ xpr[n] = parseExprInt(xpr[n]);
204
+ }
205
+ }
206
+ return xpr;
207
+ }
208
+
209
+ function binaryExpr(xpr, token, next, s, e) {
210
+ if (Array.isArray(xpr)) {
211
+ let [tl, p] = findToken(s, e);
212
+ if (p >= 0) {
213
+ let lhs = next(xpr, s, p);
214
+ let op = xpr[p];
215
+ s = p+tl;
216
+ [tl, p] = findToken(s, e);
217
+ while(p >= 0) {
218
+ let rhs = next(xpr, s, p);
219
+ lhs = array ? [ lhs, op, rhs ] : { [op]: [lhs, rhs] };
220
+ op = xpr[p];
221
+ s = p+tl;
222
+ [tl, p] = findToken(s, e);
223
+ }
224
+ return array ? [ lhs, op, next(xpr, s, e) ] : { [op]: [lhs, next(xpr, s, e)] };
225
+ }
226
+ }
227
+ return next(xpr, s, e);
228
+
229
+ function findToken(s, e) {
230
+ if(typeof token === 'function')
231
+ return token(xpr, s, e);
232
+ else {
233
+ while(s < e && !token.includes(xpr[s])) s++;
234
+ if(s < e)
235
+ return [1, s];
236
+ }
237
+ return [1, -1];
238
+ }
239
+ }
240
+
241
+ }
242
+
243
+ module.exports = {
244
+ parseExpr,
245
+ };