@sap/cds-compiler 4.3.2 → 4.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/lib/api/main.js +14 -24
  3. package/lib/api/options.js +1 -0
  4. package/lib/api/trace.js +38 -0
  5. package/lib/base/location.js +46 -1
  6. package/lib/base/message-registry.js +68 -16
  7. package/lib/base/messages.js +8 -3
  8. package/lib/checks/.eslintrc.json +1 -0
  9. package/lib/checks/actionsFunctions.js +1 -1
  10. package/lib/checks/annotationsOData.js +2 -2
  11. package/lib/checks/selectItems.js +4 -1
  12. package/lib/compiler/assert-consistency.js +3 -2
  13. package/lib/compiler/base.js +1 -1
  14. package/lib/compiler/builtins.js +25 -1
  15. package/lib/compiler/checks.js +6 -5
  16. package/lib/compiler/define.js +12 -10
  17. package/lib/compiler/extend.js +44 -23
  18. package/lib/compiler/finalize-parse-cdl.js +1 -1
  19. package/lib/compiler/generate.js +70 -53
  20. package/lib/compiler/kick-start.js +7 -5
  21. package/lib/compiler/populate.js +31 -22
  22. package/lib/compiler/propagator.js +6 -2
  23. package/lib/compiler/resolve.js +52 -17
  24. package/lib/compiler/shared.js +85 -39
  25. package/lib/compiler/tweak-assocs.js +64 -23
  26. package/lib/compiler/utils.js +40 -23
  27. package/lib/edm/.eslintrc.json +2 -0
  28. package/lib/edm/EdmPrimitiveTypeDefinitions.js +260 -0
  29. package/lib/edm/annotations/edmJson.js +994 -0
  30. package/lib/edm/annotations/genericTranslation.js +82 -423
  31. package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
  32. package/lib/edm/csn2edm.js +12 -5
  33. package/lib/edm/edm.js +14 -73
  34. package/lib/edm/edmPreprocessor.js +6 -0
  35. package/lib/gen/Dictionary.json +187 -16
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageLexer.interp +1 -1
  39. package/lib/gen/languageLexer.js +1129 -671
  40. package/lib/gen/languageParser.js +4285 -4283
  41. package/lib/json/from-csn.js +13 -18
  42. package/lib/json/to-csn.js +11 -6
  43. package/lib/language/antlrParser.js +0 -1
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/errorStrategy.js +95 -30
  46. package/lib/language/genericAntlrParser.js +21 -1
  47. package/lib/main.js +13 -3
  48. package/lib/model/csnRefs.js +42 -8
  49. package/lib/model/csnUtils.js +14 -2
  50. package/lib/model/enrichCsn.js +33 -5
  51. package/lib/model/revealInternalProperties.js +5 -0
  52. package/lib/modelCompare/compare.js +76 -14
  53. package/lib/modelCompare/utils/filter.js +19 -12
  54. package/lib/optionProcessor.js +2 -0
  55. package/lib/render/.eslintrc.json +1 -1
  56. package/lib/render/manageConstraints.js +1 -0
  57. package/lib/render/toHdbcds.js +3 -0
  58. package/lib/render/toRename.js +3 -1
  59. package/lib/render/toSql.js +46 -92
  60. package/lib/render/utils/common.js +76 -0
  61. package/lib/render/utils/delta.js +17 -3
  62. package/lib/sql-identifier.js +1 -1
  63. package/lib/transform/db/.eslintrc.json +1 -0
  64. package/lib/transform/db/applyTransformations.js +30 -4
  65. package/lib/transform/db/associations.js +22 -10
  66. package/lib/transform/db/backlinks.js +6 -2
  67. package/lib/transform/db/expansion.js +2 -2
  68. package/lib/transform/db/transformExists.js +13 -39
  69. package/lib/transform/draft/db.js +14 -3
  70. package/lib/transform/draft/odata.js +5 -18
  71. package/lib/transform/effective/associations.js +46 -15
  72. package/lib/transform/effective/main.js +7 -2
  73. package/lib/transform/effective/misc.js +43 -24
  74. package/lib/transform/effective/queries.js +20 -22
  75. package/lib/transform/effective/types.js +6 -2
  76. package/lib/transform/forOdata.js +10 -3
  77. package/lib/transform/localized.js +1 -1
  78. package/lib/transform/parseExpr.js +73 -21
  79. package/lib/transform/translateAssocsToJoins.js +22 -15
  80. package/lib/utils/term.js +2 -2
  81. package/package.json +2 -1
@@ -42,6 +42,7 @@
42
42
  const { csnRefs, artifactProperties } = require('./csnRefs');
43
43
  const { locationString } = require('../base/location');
44
44
  const { CompilerAssertion } = require('../base/error');
45
+ const { isAnnotationExpression } = require('../compiler/builtins');
45
46
  const shuffleGen = require('../base/shuffle');
46
47
 
47
48
  function enrichCsn( csn, options = {} ) {
@@ -61,7 +62,8 @@ function enrichCsn( csn, options = {} ) {
61
62
  includes: simpleRef,
62
63
  $origin,
63
64
  // TODO: excluding
64
- '@': () => { /* ignore annotations */ },
65
+ '@': assignment,
66
+ $: () => { /* ignore properties like $location for performance */ },
65
67
  };
66
68
  // options.enrichCsn = 'DEBUG';
67
69
  let $$cacheObjectNumber = 0; // for debugging
@@ -131,9 +133,30 @@ function enrichCsn( csn, options = {} ) {
131
133
  csnPath.pop();
132
134
  }
133
135
 
136
+ function assignment( parent, prop, obj ) {
137
+ if (!obj || typeof obj !== 'object' || !{}.propertyIsEnumerable.call( parent, prop ))
138
+ return;
139
+
140
+ csnPath.push( prop );
141
+ if (Array.isArray(obj)) {
142
+ obj.forEach( (n, i) => assignment( obj, i, n ) );
143
+ }
144
+ else {
145
+ const record = !isAnnotationExpression( obj ) && assignment;
146
+ // is record without `=` and other expression property
147
+ for (const name of Object.getOwnPropertyNames( obj ) ) {
148
+ const trans = record || transformers[name] || transformers[name.charAt(0)] || standard;
149
+ trans( obj, name, obj[name] );
150
+ }
151
+ if (!record && obj.$parens)
152
+ reveal( obj, '$parens', obj.$parens );
153
+ }
154
+ csnPath.pop();
155
+ }
156
+
134
157
  function refLocation( art ) {
135
158
  if (!art || typeof art !== 'object' || Array.isArray( art )) {
136
- if (!options.testMode) {
159
+ if (catchRefError()) {
137
160
  return (typeof art === 'string')
138
161
  ? `<illegal ref = ${ art }>`
139
162
  : `<illegal ref: ${ typeof art }>`;
@@ -144,14 +167,14 @@ function enrichCsn( csn, options = {} ) {
144
167
  return art.$location;
145
168
  }
146
169
 
147
- if (!options.testMode)
170
+ if (catchRefError())
148
171
  return `<${ Object.keys( art ).join('+') }+!$location>`;
149
172
  throw new CompilerAssertion( 'Reference to object without $location' );
150
173
  }
151
174
 
152
175
  function simpleRef( parent, prop, ref ) {
153
176
  // try {
154
- const notFound = (options.testMode) ? undefined : null;
177
+ const notFound = (catchRefError()) ? null : undefined;
155
178
  if (Array.isArray( ref )) {
156
179
  parent[`_${ prop }`] = ref.map( r => refLocation( artifactRef( r, notFound ) ) );
157
180
  }
@@ -220,7 +243,7 @@ function enrichCsn( csn, options = {} ) {
220
243
  }
221
244
 
222
245
  function handleError( callback ) {
223
- if (options.testMode)
246
+ if (!catchRefError())
224
247
  return callback();
225
248
  try {
226
249
  return callback();
@@ -335,6 +358,11 @@ function enrichCsn( csn, options = {} ) {
335
358
  setLocations( node[name], isMember || name, loc );
336
359
  }
337
360
  }
361
+
362
+ function catchRefError() {
363
+ return !options.testMode || // false &&
364
+ csnPath.some( p => typeof p === 'string' && p.charAt(0) === '@');
365
+ }
338
366
  }
339
367
 
340
368
 
@@ -104,6 +104,8 @@ function revealInternalProperties( model, nameOrPath ) {
104
104
  $messageFunctions: () => '‹some functions›',
105
105
  $functions: () => '‹some functions›',
106
106
  $builtins: nameOrPath === '++' ? builtinsDictionary : () => '‹reveal with -R ++›',
107
+ tokenStream: ts => `‹${ ts?.tokens?.length ?? '?' } tokens›`,
108
+ parseListener: _ => '‹parseListener›',
107
109
  };
108
110
  uniqueId = -1;
109
111
  return revealXsnPath(nameOrPath, model);
@@ -292,6 +294,7 @@ function revealInternalProperties( model, nameOrPath ) {
292
294
  }
293
295
 
294
296
  function targetAspect( node, parent ) {
297
+ // TODO: avoid repeated display of same target aspect (includes)
295
298
  if (node.elements && node.__unique_id__ == null && node.$effectiveSeqNo == null)
296
299
  Object.defineProperty( node, '__unique_id__', { value: uniqueId-- } );
297
300
  return reveal( node, parent );
@@ -339,6 +342,8 @@ function artifactIdentifier( node, parent ) {
339
342
  }
340
343
  switch (node.kind) {
341
344
  case undefined:
345
+ if (node.name.id === '$self' && node.location.file === '')
346
+ return `type:${ quoted( '$self' ) }##0`;
342
347
  return (node._artifact && node._artifact.kind)
343
348
  ? artifactIdentifier( node._artifact )
344
349
  : JSON.stringify(node.name);
@@ -7,8 +7,7 @@ const {
7
7
  isPersistedAsTable,
8
8
  isPersistedAsView
9
9
  } = require('../model/csnUtils');
10
- const { isBetaEnabled } = require('../base/model');
11
- const { forEachKey } = require('../utils/objectUtils');
10
+ const { forEachKey, forEach } = require('../utils/objectUtils');
12
11
 
13
12
  // used to mark a view as changed so we know to drop-create it
14
13
  const isChanged = Symbol('Marks a view as changed');
@@ -33,6 +32,7 @@ function compareModels(beforeModel, afterModel, options) {
33
32
  returnObj.extensions = [];
34
33
  returnObj.migrations = []; // element changes/removals or changes of entity properties
35
34
  returnObj.unchangedConstraints = new Set();
35
+ returnObj.changedPrimaryKeys = [];
36
36
 
37
37
  // There is currently no use in knowing the added entities only. If this changes, hand in `addedEntities` to `getArtifactComparator` below.
38
38
  forEachDefinition(afterModel, getExtensionAndMigrations(beforeModel, options, returnObj));
@@ -74,21 +74,43 @@ function validateCsnVersions(beforeModel, afterModel, options) {
74
74
  * @param {object} returnObj
75
75
  * @returns {function}
76
76
  */
77
- function getExtensionAndMigrations(beforeModel, options, { extensions, migrations, unchangedConstraints }) {
77
+ function getExtensionAndMigrations(beforeModel, options, { extensions, migrations, unchangedConstraints, changedPrimaryKeys }) {
78
78
  return function compareArtifacts(artifact, name) {
79
+ let hasPrimaryKeyChange = false;
80
+
79
81
  function addElements() {
80
82
  const elements = {};
81
- forEachMember(artifact, getElementComparator(otherArtifact, elements), [ 'definitions', name ], true, { elementsOnly: true });
83
+ const keysNow = [];
84
+ forEachMember(artifact, (element, eName) => {
85
+ getElementComparator(otherArtifact, elements)(element, eName);
86
+ if(element.key)
87
+ keysNow.push(eName);
88
+ }, [ 'definitions', name ], true, { elementsOnly: true });
89
+
90
+ // Only do this check for to.hdi.migration - the order only "bites" us when doing .hdbmigrationtable as the end-check against the intended
91
+ // create-table will fail. TODO: Does a mismatched order of the primary key hurt us for postgres and others?
92
+ if(!hasPrimaryKeyChange && options.sqlDialect === 'hana' && options.src === 'hdi') {
93
+ const keysOther = [];
94
+ forEachMember(otherArtifact, (element, eName) => {
95
+ if(element.key)
96
+ keysOther.push(eName);
97
+ }, [ 'definitions', name ], true, { elementsOnly: true });
98
+
99
+ if(keysNow.join(',') !== keysOther.join(','))
100
+ hasPrimaryKeyChange = true;
101
+ }
102
+
82
103
  if (Object.keys(elements).length > 0) {
83
- extensions.push(addedElements(name, elements));
104
+ const added = addedElements(name, elements);
105
+ if(!hasPrimaryKeyChange)
106
+ forEach(added.elements, (_name, element) => {
107
+ if(element.key && !element.target)
108
+ hasPrimaryKeyChange = true;
109
+ });
110
+ extensions.push(added);
84
111
  }
85
112
  }
86
113
  function changePropsOrRemoveOrChangeElements() {
87
- const relevantProperties = [
88
- { name: 'doc' },
89
- { name: '@sql.prepend' },
90
- { name: '@sql.append' },
91
- ];
92
114
  const changedProperties = {};
93
115
 
94
116
  const removedElements = {};
@@ -96,9 +118,9 @@ function getExtensionAndMigrations(beforeModel, options, { extensions, migration
96
118
 
97
119
  const migration = { migrate: name };
98
120
 
99
- relevantProperties.forEach(prop => {
100
- if (artifact[prop.name] !== otherArtifact[prop.name] && (!prop.beta || isBetaEnabled(options, prop.beta))) {
101
- changedProperties[prop.name] = changedElement(artifact[prop.name], otherArtifact[prop.name] || null);
121
+ Object.keys(relevantProperties).forEach(prop => {
122
+ if (artifact[prop] !== otherArtifact[prop]) {
123
+ changedProperties[prop] = changedElement(artifact[prop], otherArtifact[prop] || null);
102
124
  }
103
125
  });
104
126
 
@@ -156,11 +178,25 @@ function getExtensionAndMigrations(beforeModel, options, { extensions, migration
156
178
  forEachMember(otherArtifact, getElementComparator(artifact, removedElements), [ 'definitions', name ], true, { elementsOnly: true });
157
179
  if (Object.keys(removedElements).length > 0) {
158
180
  migration.remove = removedElements;
181
+ if(!hasPrimaryKeyChange)
182
+ forEach(removedElements, (_name, change) => {
183
+ if(change.key && !change.target)
184
+ hasPrimaryKeyChange = true;
185
+ });
159
186
  }
160
187
 
161
188
  forEachMember(artifact, getElementComparator(otherArtifact, null, changedElements), [ 'definitions', name ], true, { elementsOnly: true });
162
189
  if (Object.keys(changedElements).length > 0) {
163
190
  migration.change = changedElements;
191
+ if(!hasPrimaryKeyChange)
192
+ forEach(changedElements, (_name, change) => {
193
+ if((change.old.key || change.new.key) && !change.new.target && !change.old.target) {
194
+ // For to.hdi.migration: Just drop-create (commented out), for to.sql.migration: Handle case where we add/remove "key" keyword, no drop-create otherwise
195
+ if(options.sqlDialect === 'hana' && options.src === 'hdi' || (!change.old.key || !change.new.key)) {
196
+ hasPrimaryKeyChange = true;
197
+ }
198
+ }
199
+ });
164
200
  }
165
201
 
166
202
  if (migration.properties || migration.remove || migration.change || migration.removeConstraints) {
@@ -193,6 +229,9 @@ function getExtensionAndMigrations(beforeModel, options, { extensions, migration
193
229
 
194
230
  addElements();
195
231
  changePropsOrRemoveOrChangeElements();
232
+ if(hasPrimaryKeyChange) {
233
+ changedPrimaryKeys.push(name);
234
+ }
196
235
  };
197
236
  }
198
237
  /**
@@ -317,6 +356,13 @@ function deepEqual(a, b, include = () => true, depth = 0) {
317
356
  : a === b;
318
357
  }
319
358
 
359
+
360
+ const relevantProperties = {
361
+ 'doc': true,
362
+ '@sql.prepend': true,
363
+ '@sql.append': true
364
+ };
365
+
320
366
  /**
321
367
  * Returns whether any type parameters differ between two given elements. Ignores whether types themselves differ (`type` property).
322
368
  * @param element {object} an element
@@ -324,7 +370,23 @@ function deepEqual(a, b, include = () => true, depth = 0) {
324
370
  * @returns {boolean}
325
371
  */
326
372
  function typeParametersChanged(element, otherElement) {
327
- return !deepEqual(element, otherElement, (key, depth) => !(depth === 0 && key === 'type'));
373
+ const checked = new Set();
374
+ for (const key in element) {
375
+ if (Object.prototype.hasOwnProperty.call(element, key))
376
+ if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type') {
377
+ checked.add(key);
378
+ if(!deepEqual(element[key], otherElement[key]))
379
+ return true;
380
+ }
381
+ }
382
+
383
+ for (const key in otherElement) {
384
+ if (Object.prototype.hasOwnProperty.call(otherElement, key))
385
+ if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type' && !checked.has(key))
386
+ return true;
387
+ }
388
+
389
+ return false;
328
390
  }
329
391
 
330
392
  function addedElements(entity, elements) {
@@ -38,6 +38,9 @@ module.exports = {
38
38
  warning('def-unsupported-constraint-drop', [ 'definitions', constraint.parentTable, 'elements', constraint.paths ? name : name.slice(constraint.parentTable.length + 1) ], { id: constraint.identifier || name, name: 'sqlite' },
39
39
  'Ignoring drop of constraint $(ID), as this is not supported for dialect $(NAME); you need to manually resolve this via shadow tables and data copy');
40
40
  delete constraintRemovals[name];
41
+ },
42
+ function primaryKey() {
43
+ return false;
41
44
  }
42
45
  ),
43
46
  postgres: getFilterObject('postgres'),
@@ -46,7 +49,7 @@ module.exports = {
46
49
  csn: filterCsn,
47
50
  };
48
51
 
49
- function getFilterObject( dialect, extensionCallback, migrationCallback, removeConstraintsCallback ) {
52
+ function getFilterObject( dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) {
50
53
  return {
51
54
  // will be called with a simple Array.filter, as we need to filter constraint `ADD` for SQLite
52
55
  extension: ({
@@ -66,14 +69,12 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
66
69
  return returnValue;
67
70
  },
68
71
  // will be called with a Array.forEach
69
- migration: ({
70
- change, migrate, remove, removeConstraints,
71
- }, { error, warning, message }) => {
72
- forEach(remove, (name) => {
73
- error('def-unsupported-element-drop', [ 'definitions', migrate, 'elements', name ], {}, 'Dropping elements is not supported');
72
+ migration: (migrations, { error, warning, message }) => {
73
+ forEach(migrations.remove, (name) => {
74
+ error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported');
74
75
  });
75
- forEach(change, (name, migration) => {
76
- const loc = [ 'definitions', migrate, 'elements', name ];
76
+ forEach(migrations.change, (name, migration) => {
77
+ const loc = [ 'definitions', migrations.migrate, 'elements', name ];
77
78
  if (migration.new.type === migration.old.type && migration.new.length < migration.old.length)
78
79
  error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported');
79
80
  else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale)
@@ -83,9 +84,9 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
83
84
  else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type))
84
85
  error('type-unsupported-change', loc, { id: name, name: migration.old.type, type: migration.new.type }, 'Changed element $(ID) is a lossy type change from $(NAME) to $(TYPE) and is not supported');
85
86
  else if (dialect !== 'sqlite' && isKey(migration.new) && !isKey(migration.old)) // key added/changed - pg, hana and sqlite do not support it, h2 probably also - issues when data is in the table already
86
- message('type-unsupported-key-change', [ 'definitions', migrate, 'elements', name ], { id: name, '#': 'changed' } );
87
+ message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } );
87
88
  else if (migrationCallback)
88
- migrationCallback(migrate, name, migration, change, error);
89
+ migrationCallback(migrations.migrate, name, migration, migrations.change, error);
89
90
 
90
91
  // TODO: precision/scale growth
91
92
  });
@@ -93,8 +94,8 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
93
94
  if (removeConstraintsCallback) {
94
95
  const constraintTypes = [ 'unique', 'referential' ];
95
96
  constraintTypes.forEach((constraintType) => {
96
- forEach(removeConstraints?.[constraintType], (name, constraint) => {
97
- removeConstraintsCallback(removeConstraints[constraintType], name, constraint, warning);
97
+ forEach(migrations.removeConstraints?.[constraintType], (name, constraint) => {
98
+ removeConstraintsCallback(migrations.removeConstraints[constraintType], name, constraint, warning);
98
99
  });
99
100
  });
100
101
  }
@@ -103,6 +104,12 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
103
104
  if (isPersistedAsTable(artifact))
104
105
  error('def-unsupported-table-drop', [ 'definitions', artifactName ], 'Dropping tables is not supported');
105
106
  },
107
+ changedPrimaryKeys: (changedPrimaryKeyArtifactName) => {
108
+ if (primaryKeyCallback)
109
+ return primaryKeyCallback(changedPrimaryKeyArtifactName);
110
+
111
+ return true;
112
+ },
106
113
  };
107
114
  }
108
115
 
@@ -227,6 +227,7 @@ optionProcessor.command('O, toOdata')
227
227
  .option(' --odata-foreign-keys')
228
228
  .option(' --odata-v2-partial-constr')
229
229
  .option(' --odata-vocabularies <list>')
230
+ .option(' --odata-open-type')
230
231
  .option('-c, --csn')
231
232
  .option('-f, --odata-format <format>', ['flat', 'structured'])
232
233
  .option('-n, --sql-mapping <style>', ['plain', 'quoted', 'hdbcds'], { aliases: [ '--names' ] })
@@ -259,6 +260,7 @@ optionProcessor.command('O, toOdata')
259
260
  (Not spec compliant and V2 only)
260
261
  --odata-vocabularies <list> JSON array of adhoc vocabulary definitions
261
262
  { prefix: { alias, ns, uri }, ... }
263
+ --odata-open-type Renders all structured types as OpenType=true, if not annotated otherwise.
262
264
  -n, --sql-mapping <style> Annotate artifacts and elements with "@cds.persistence.name", which is
263
265
  the corresponding database name (see "--sql-mapping" for "toHana or "toSql")
264
266
  plain : (default) Names in uppercase and flattened with underscores
@@ -2,6 +2,7 @@
2
2
  "plugins": ["sonarjs"],
3
3
  "extends": ["../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended"],
4
4
  "rules": {
5
+ "cds-compiler/message-no-quotes": "off",
5
6
  "prefer-const": "error",
6
7
  "quotes": ["error", "single", "avoid-escape"],
7
8
  "prefer-template": "error",
@@ -11,7 +12,6 @@
11
12
  "sonarjs/no-nested-template-literals": "off",
12
13
  "template-curly-spacing":["error", "never"],
13
14
  "class-methods-use-this": "off",
14
- // Who cares - just very whiny and in the way
15
15
  "complexity": "off",
16
16
  "max-len": "off",
17
17
  "no-shadow": "warn"
@@ -67,6 +67,7 @@ function _transformSqlOptions( model, options ) {
67
67
  const { warning, error } = makeMessageFunction(model, options, 'to.sql');
68
68
 
69
69
  optionProcessor.verifyOptions(options, 'toSql', true)
70
+ // eslint-disable-next-line cds-compiler/message-template-string
70
71
  .forEach(complaint => warning(null, null, `${complaint}`));
71
72
 
72
73
  if (options.sqlDialect !== 'hana') {
@@ -1336,6 +1336,9 @@ function toHdbcdsSource( csn, options ) {
1336
1336
 
1337
1337
  else if (x.ref[1] === 'locale')
1338
1338
  return 'SESSION_CONTEXT(\'LOCALE\')';
1339
+
1340
+ else if (x.ref[1] === 'tenant')
1341
+ return 'SESSION_CONTEXT(\'TENANT\')';
1339
1342
  }
1340
1343
  else if (x.ref[0] === '$at' || x.ref[0] === '$valid') {
1341
1344
  if (x.ref[1] === 'from')
@@ -34,7 +34,9 @@ function toRename( inputCsn, options ) {
34
34
  options = Object.assign({ sqlMapping: 'hdbcds', sqlDialect: 'hana' }, options);
35
35
 
36
36
  // Verify options
37
- optionProcessor.verifyOptions(options, 'toRename').forEach(complaint => warning(null, null, `${complaint}`));
37
+ optionProcessor.verifyOptions(options, 'toRename')
38
+ // eslint-disable-next-line cds-compiler/message-template-string
39
+ .forEach(complaint => warning(null, null, `${complaint}`));
38
40
  checkCSNVersion(inputCsn, options);
39
41
 
40
42
  // Let users know that this is internal
@@ -10,6 +10,7 @@ const { forEach, forEachValue, forEachKey } = require('../utils/objectUtils');
10
10
  const {
11
11
  renderFunc, cdsToSqlTypes, getHanaComment, hasHanaComment,
12
12
  getSqlSnippets, createExpressionRenderer, withoutCast,
13
+ variableForDialect,
13
14
  } = require('./utils/common');
14
15
  const {
15
16
  getDeltaRenderer,
@@ -106,7 +107,7 @@ function toSqlDdl( csn, options ) {
106
107
  error, warning, info, throwWithAnyError,
107
108
  } = makeMessageFunction(csn, options, 'to.sql');
108
109
  const { quoteSqlId, prepareIdentifier, renderArtifactName } = getIdentifierUtils(csn, options);
109
- let reportedMissingUserReplacement = false;
110
+ const reportedMissingReplacements = Object.create(null);
110
111
 
111
112
  const exprRenderer = createExpressionRenderer({
112
113
  // FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
@@ -157,6 +158,7 @@ function toSqlDdl( csn, options ) {
157
158
  renderStringForSql,
158
159
  activateAlterMode,
159
160
  getHanaComment,
161
+ renderExpr,
160
162
  });
161
163
 
162
164
  // FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect
@@ -197,6 +199,15 @@ function toSqlDdl( csn, options ) {
197
199
  for (const artifactName in csn.deletions)
198
200
  renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], mainResultObj);
199
201
 
202
+
203
+ if (csn.changedPrimaryKeys && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
204
+ csn.changedPrimaryKeys = options.testMode ? sortCsn(csn.changedPrimaryKeys) : csn.changedPrimaryKeys;
205
+ csn.changedPrimaryKeys.forEach((artifactName) => {
206
+ const drop = render.dropKey(artifactName);
207
+ addMigration(mainResultObj, artifactName, true, render.concat(...drop));
208
+ });
209
+ }
210
+
200
211
  // Render each artifact extension
201
212
  // Only SAP HANA SQL is currently supported.
202
213
  // Note that extensions may contain new elements referenced in migrations, thus should be compiled first.
@@ -223,11 +234,19 @@ function toSqlDdl( csn, options ) {
223
234
  // There is no "migrations" property in client CSN, so for better locations, use
224
235
  // a path to the definition.
225
236
  const env = new SqlRenderEnvironment({ path: [ 'definitions', artifactName ] });
226
- renderArtifactMigrationInto(artifactName, migration, mainResultObj, env);
237
+ renderArtifactMigrationInto(artifactName, csn.definitions[artifactName], migration, mainResultObj, env);
227
238
  }
228
239
  }
229
240
  }
230
241
 
242
+ if (csn.changedPrimaryKeys && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
243
+ csn.changedPrimaryKeys = options.testMode ? sortCsn(csn.changedPrimaryKeys) : csn.changedPrimaryKeys;
244
+ csn.changedPrimaryKeys.forEach((artifactName) => {
245
+ const add = render.addKey(artifactName, csn.definitions[artifactName].elements);
246
+ addMigration(mainResultObj, artifactName, true, render.concat(...add));
247
+ });
248
+ }
249
+
231
250
  // trigger artifact and element name checks
232
251
  definitionsDuplicateChecker.check(error, options);
233
252
  extensionsDuplicateChecker.check(error);
@@ -360,7 +379,7 @@ function toSqlDdl( csn, options ) {
360
379
 
361
380
  // Render an artifact migration into the appropriate dictionary of 'resultObj'.
362
381
  // Only SAP HANA SQL is currently supported.
363
- function renderArtifactMigrationInto( artifactName, migration, resultObj, env ) {
382
+ function renderArtifactMigrationInto( artifactName, artifact, migration, resultObj, env ) {
364
383
  function reducesTypeSize( def ) {
365
384
  // HANA does not allow decreasing the value of any of those type parameters.
366
385
  return def.old.type === def.new.type &&
@@ -501,9 +520,8 @@ function toSqlDdl( csn, options ) {
501
520
  : render.addColumnsFromElementsObj(artifactName, { [eltName]: def.new }, env);
502
521
  addMigration(resultObj, artifactName, true, render.concat(...drop, ...add));
503
522
  }
504
- else {
505
- // Lossless change: no associations directly affected, no size reduction.
506
- addMigration(resultObj, artifactName, false, render.alterColumns(artifactName, sqlId, def, eltStrNew));
523
+ else { // Lossless change: no associations directly affected, no size reduction.
524
+ addMigration(resultObj, artifactName, false, render.alterColumns(artifactName, sqlId, def, eltStrNew, eltName, activateAlterMode(env, 'migration')));
507
525
  }
508
526
  }
509
527
  }
@@ -649,12 +667,6 @@ function toSqlDdl( csn, options ) {
649
667
  const associations = render.addAssociations(artifactName, extElements, env);
650
668
  if (elements.length + associations.length > 0)
651
669
  addMigration(resultObj, artifactName, false, [ ...elements, ...associations ]);
652
-
653
- if (Object.values(extElements).some(elt => elt.key)) {
654
- const drop = render.dropKey(artifactName);
655
- const add = render.addKey(artifactName, artifactElements);
656
- addMigration(resultObj, artifactName, true, render.concat(...drop, ...add));
657
- }
658
670
  }
659
671
 
660
672
  function addMigration( resultObj, artifactName, drop, sqlArray, description ) {
@@ -1529,95 +1541,37 @@ function toSqlDdl( csn, options ) {
1529
1541
  * @returns {string|null} Null in case of an invalid second path step
1530
1542
  */
1531
1543
  function render$user( x ) {
1532
- // FIXME: this is all not enough: we might need an explicit select item alias (?)
1533
- if (x.ref[1] === 'id') {
1534
- if (options.sqlDialect === 'hana')
1535
- return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1536
- else if (options.sqlDialect === 'postgres')
1537
- return 'current_setting(\'cap.applicationuser\')';
1538
- else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1539
- return 'session_context( \'$user.id\' )';
1540
- else if (options.sqlDialect === 'h2')
1541
- return '@applicationuser';
1542
-
1543
- if (!reportedMissingUserReplacement) {
1544
- warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1545
- reportedMissingUserReplacement = true;
1546
- }
1547
- return '\'$user.id\'';
1548
- }
1549
- else if (x.ref[1] === 'locale') {
1550
- if (options.sqlDialect === 'hana')
1551
- return 'SESSION_CONTEXT(\'LOCALE\')';
1552
- else if (options.sqlDialect === 'postgres')
1553
- return 'current_setting(\'cap.locale\')';
1554
- else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1555
- return 'session_context( \'$user.locale\' )';
1556
- else if (options.sqlDialect === 'h2')
1557
- return '@locale';
1558
-
1559
- return '\'en\''; // default language
1544
+ if (x.ref.length > 2 || typeof x.ref[1] !== 'string')
1545
+ return null; // `$user` can only have two path steps
1546
+
1547
+ const name = `$user.${x.ref[1]}`;
1548
+ const result = variableForDialect(options, name);
1549
+ if (result)
1550
+ return result;
1551
+
1552
+ if (!reportedMissingReplacements[name]) {
1553
+ reportedMissingReplacements[name] = true;
1554
+ warning('ref-unsupported-variable', null, { name, option: 'variableReplacements' },
1555
+ 'Variable $(NAME) is not supported. Use option $(OPTION) to specify a value for $(NAME)');
1560
1556
  }
1561
- // Basically: Second path step was invalid, do nothing - should not happen.
1562
- return null;
1557
+ return `'${name}'`;
1563
1558
  }
1564
1559
 
1565
1560
  /**
1566
- * For a given reference starting with $at, render a 'current_timestamp' literal for plain.
1567
- * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1568
- *
1569
- *
1570
- * For sqlite, we render the string-format-time (strftime) function.
1571
- * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1572
- * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1573
- * --> Therefore the comparison in the temporal where clause doesn't work properly.
1574
- *
1575
1561
  * @param {object} x
1576
1562
  * @returns {string|null} Null in case of an invalid second path step
1577
1563
  */
1578
1564
  function render$at( x ) {
1579
- if (x.ref[1] === 'from') {
1580
- switch (options.sqlDialect) {
1581
- case 'sqlite': {
1582
- if (options.betterSqliteSessionVariables)
1583
- return 'session_context( \'$valid.from\' )';
1584
- const dateFromFormat = '%Y-%m-%dT%H:%M:%S.000Z';
1585
- return `strftime('${dateFromFormat}', 'now')`;
1586
- }
1587
- case 'hana':
1588
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1589
- case 'postgres':
1590
- return 'current_setting(\'cap.valid_from\')::timestamp';
1591
- case 'h2':
1592
- return '@valid_from';
1593
- case 'plain':
1594
- return 'current_timestamp';
1595
- default:
1596
- break;
1597
- }
1598
- }
1565
+ if (x.ref.length > 2 || typeof x.ref[1] !== 'string')
1566
+ return null; // `$at` can only have two path steps
1599
1567
 
1600
- if (x.ref[1] === 'to') {
1601
- switch (options.sqlDialect) {
1602
- case 'sqlite': {
1603
- if (options.betterSqliteSessionVariables)
1604
- return 'session_context( \'$valid.to\' )';
1605
- // + 1ms compared to $at.from
1606
- const dateToFormat = '%Y-%m-%dT%H:%M:%S.001Z';
1607
- return `strftime('${dateToFormat}', 'now')`;
1608
- }
1609
- case 'hana':
1610
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1611
- case 'postgres':
1612
- return 'current_setting(\'cap.valid_to\')::timestamp';
1613
- case 'h2':
1614
- return '@valid_to';
1615
- case 'plain':
1616
- return 'current_timestamp';
1617
- default:
1618
- break;
1619
- }
1620
- }
1568
+ const name = `$at.${x.ref[1]}`;
1569
+ const config = variableForDialect(options, name);
1570
+ if (config)
1571
+ return config;
1572
+
1573
+ if (options.testMode)
1574
+ throw new CompilerAssertion(`render$at: unhandled sqlDialect '${options.sqlDialect}' when rendering ${x.ref.join('.')}`);
1621
1575
  return null;
1622
1576
  }
1623
1577