@sap/cds-compiler 2.13.8 → 2.15.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 (78) hide show
  1. package/CHANGELOG.md +109 -4
  2. package/bin/cdsc.js +112 -37
  3. package/lib/api/main.js +20 -22
  4. package/lib/api/options.js +2 -3
  5. package/lib/api/validate.js +6 -6
  6. package/lib/base/message-registry.js +89 -14
  7. package/lib/base/messages.js +85 -64
  8. package/lib/base/optionProcessorHelper.js +19 -0
  9. package/lib/checks/annotationsOData.js +11 -32
  10. package/lib/checks/arrayOfs.js +1 -34
  11. package/lib/checks/validator.js +2 -4
  12. package/lib/compiler/assert-consistency.js +1 -0
  13. package/lib/compiler/base.js +1 -0
  14. package/lib/compiler/builtins.js +11 -0
  15. package/lib/compiler/checks.js +22 -70
  16. package/lib/compiler/define.js +59 -11
  17. package/lib/compiler/extend.js +20 -3
  18. package/lib/compiler/finalize-parse-cdl.js +26 -20
  19. package/lib/compiler/index.js +75 -26
  20. package/lib/compiler/populate.js +6 -5
  21. package/lib/compiler/propagator.js +4 -1
  22. package/lib/compiler/resolve.js +104 -16
  23. package/lib/compiler/shared.js +61 -27
  24. package/lib/compiler/tweak-assocs.js +7 -1
  25. package/lib/edm/annotations/genericTranslation.js +33 -15
  26. package/lib/edm/csn2edm.js +216 -98
  27. package/lib/edm/edm.js +298 -225
  28. package/lib/edm/edmPreprocessor.js +486 -415
  29. package/lib/edm/edmUtils.js +22 -22
  30. package/lib/gen/Dictionary.json +90 -16
  31. package/lib/gen/language.checksum +1 -1
  32. package/lib/gen/language.interp +3 -1
  33. package/lib/gen/languageParser.js +4636 -4368
  34. package/lib/json/csnVersion.js +10 -11
  35. package/lib/json/from-csn.js +3 -2
  36. package/lib/json/to-csn.js +0 -2
  37. package/lib/language/docCommentParser.js +2 -2
  38. package/lib/language/genericAntlrParser.js +47 -2
  39. package/lib/language/language.g4 +59 -27
  40. package/lib/main.d.ts +19 -1
  41. package/lib/main.js +6 -0
  42. package/lib/model/csnRefs.js +33 -6
  43. package/lib/model/csnUtils.js +193 -75
  44. package/lib/model/enrichCsn.js +1 -0
  45. package/lib/model/revealInternalProperties.js +2 -2
  46. package/lib/modelCompare/compare.js +6 -6
  47. package/lib/optionProcessor.js +62 -26
  48. package/lib/render/toCdl.js +844 -679
  49. package/lib/render/toHdbcds.js +189 -243
  50. package/lib/render/toSql.js +180 -198
  51. package/lib/render/utils/common.js +131 -15
  52. package/lib/transform/db/.eslintrc.json +1 -1
  53. package/lib/transform/db/associations.js +2 -2
  54. package/lib/transform/db/constraints.js +3 -1
  55. package/lib/transform/db/expansion.js +15 -10
  56. package/lib/transform/db/flattening.js +94 -64
  57. package/lib/transform/db/transformExists.js +7 -7
  58. package/lib/transform/db/views.js +6 -3
  59. package/lib/transform/forHanaNew.js +43 -26
  60. package/lib/transform/forOdataNew.js +43 -42
  61. package/lib/transform/localized.js +12 -7
  62. package/lib/transform/odata/toFinalBaseType.js +5 -5
  63. package/lib/transform/odata/typesExposure.js +145 -197
  64. package/lib/transform/transformUtilsNew.js +9 -12
  65. package/lib/transform/translateAssocsToJoins.js +1 -1
  66. package/lib/transform/universalCsn/coreComputed.js +5 -3
  67. package/lib/transform/universalCsn/universalCsnEnricher.js +27 -5
  68. package/lib/utils/moduleResolve.js +13 -6
  69. package/package.json +1 -1
  70. package/share/messages/message-explanations.json +2 -1
  71. package/share/messages/syntax-expected-integer.md +37 -0
  72. package/lib/transform/odata/attachPath.js +0 -96
  73. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  74. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  75. package/lib/transform/odata/referenceFlattener.js +0 -296
  76. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  77. package/lib/transform/odata/structuralPath.js +0 -72
  78. package/lib/transform/odata/structureFlattener.js +0 -171
@@ -7,8 +7,8 @@ const {
7
7
  forEachDefinition, getResultingName, getVariableReplacement,
8
8
  } = require('../model/csnUtils');
9
9
  const {
10
- renderFunc, beautifyExprArray, cdsToSqlTypes, getHanaComment, hasHanaComment,
11
- getSqlSnippets,
10
+ renderFunc, cdsToSqlTypes, getHanaComment, hasHanaComment,
11
+ getSqlSnippets, getExpressionRenderer,
12
12
  } = require('./utils/common');
13
13
  const {
14
14
  renderReferentialConstraint, getIdentifierUtils,
@@ -77,9 +77,37 @@ const { ModelError } = require('../base/error');
77
77
  function toSqlDdl(csn, options) {
78
78
  timetrace.start('SQL rendering');
79
79
  const {
80
- error, warning, info, throwWithError,
80
+ error, warning, info, throwWithAnyError,
81
81
  } = makeMessageFunction(csn, options, 'to.sql');
82
82
  const { quoteSqlId, prepareIdentifier } = getIdentifierUtils(options);
83
+ const renderExpr = getExpressionRenderer({
84
+ finalize: x => String(x).toUpperCase(),
85
+ explicitTypeCast: (x, env) => {
86
+ const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast);
87
+ return `CAST(${renderExpr(x, env)} AS ${typeRef})`;
88
+ },
89
+ val: renderExpressionLiteral,
90
+ enum: (x) => {
91
+ // TODO: Signal is not covered by tests + better location
92
+ // FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand)
93
+ error(null, x.$location, 'Enum values are not yet supported for conversion to SQL');
94
+ return '';
95
+ },
96
+ ref: renderExpressionRef,
97
+ aliasOnly(x, _env) {
98
+ return x.as;
99
+ },
100
+ windowFunction: (x, env) => renderWindowFunction(smartFuncId(prepareIdentifier(x.func), options.toSql.dialect), x, env),
101
+ func: (x, env) => renderFunc(smartFuncId(prepareIdentifier(x.func), options.toSql.dialect), x, options.toSql.dialect, a => renderArgs(a, '=>', env, null)),
102
+ xpr(x, env) {
103
+ if (this.nestedExpr && !x.cast)
104
+ return `(${renderExpr(x.xpr, env, this.inline, true)})`;
105
+
106
+ return renderExpr(x.xpr, env, this.inline, true);
107
+ },
108
+ SELECT: (x, env) => `(${renderQuery('<subselect>', x, increaseIndent(env))})`,
109
+ SET: (x, env) => `(${renderQuery('<union>', x, increaseIndent(env))})`,
110
+ });
83
111
 
84
112
  // Utils to render SQL statements.
85
113
  const render = {
@@ -89,7 +117,8 @@ function toSqlDdl(csn, options) {
89
117
  */
90
118
  addColumns: {
91
119
  fromElementStrings(tableName, eltStrings) {
92
- return [ `ALTER TABLE ${tableName} ADD (${eltStrings.join(', ')});` ];
120
+ const elts = options.sqlDialect === 'hana' ? `(${eltStrings.join(', ')})` : `${eltStrings.join(', ')}`;
121
+ return [ `ALTER TABLE ${tableName} ADD ${elts};` ];
93
122
  },
94
123
  fromElementsObj(artifactName, tableName, elementsObj, env, duplicateChecker) {
95
124
  // Only extend with 'ADD' for elements/associations
@@ -171,7 +200,7 @@ function toSqlDdl(csn, options) {
171
200
  Render comment string.
172
201
  */
173
202
  comment(comment) {
174
- return comment && `'${comment.replace(/'/g, '\'\'')}'` || 'NULL';
203
+ return comment && renderStringForSql(comment, options.sqlDialect) || 'NULL';
175
204
  },
176
205
  /*
177
206
  Alter SQL snippet for entity.
@@ -233,7 +262,7 @@ function toSqlDdl(csn, options) {
233
262
  // Render each artifact extension
234
263
  // Only HANA SQL is currently supported.
235
264
  // Note that extensions may contain new elements referenced in migrations, thus should be compiled first.
236
- if (csn.extensions && options.toSql.dialect === 'hana') {
265
+ if (csn.extensions && (options.toSql.dialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
237
266
  for (const extension of options && options.testMode ? sortCsn(csn.extensions) : csn.extensions) {
238
267
  if (extension.extend) {
239
268
  const artifactName = extension.extend;
@@ -246,7 +275,7 @@ function toSqlDdl(csn, options) {
246
275
 
247
276
  // Render each artifact change
248
277
  // Only HANA SQL is currently supported.
249
- if (csn.migrations && options.toSql.dialect === 'hana') {
278
+ if (csn.migrations && (options.toSql.dialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
250
279
  for (const migration of options && options.testMode ? sortCsn(csn.migrations) : csn.migrations) {
251
280
  if (migration.migrate) {
252
281
  const artifactName = migration.migrate;
@@ -263,7 +292,7 @@ function toSqlDdl(csn, options) {
263
292
  deletionsDuplicateChecker.check(error);
264
293
 
265
294
  // Throw exception in case of errors
266
- throwWithError();
295
+ throwWithAnyError();
267
296
 
268
297
  // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src === 'sql'
269
298
  // (relying on the order of dictionaries above)
@@ -416,13 +445,13 @@ function toSqlDdl(csn, options) {
416
445
  function oldAnnoChangedIncompatibly(defOld, defNew) {
417
446
  return typeof defOld === 'string' && defOld.trim().length && !(typeof defNew === 'string' && defNew.trim().startsWith(`${defOld.trim()} `));
418
447
  }
419
- function getUnknownSqlReason(anno, artifactName, defOld, defNew, eltName) {
448
+ function getUnknownSqlReason(anno, artName, defOld, defNew, eltName) {
420
449
  const changeKind = defNew === undefined
421
450
  ? `removed (previous value: ${JSON.stringify(defOld)})`
422
451
  : `changed from ${JSON.stringify(defOld)} to ${JSON.stringify(defNew)}`;
423
452
  return eltName
424
- ? `annotation ${anno} of element ${artifactName}:${eltName} has been ${changeKind}`
425
- : `annotation ${anno} of artifact ${artifactName} has been ${changeKind}`;
453
+ ? `annotation ${anno} of element ${artName}:${eltName} has been ${changeKind}`
454
+ : `annotation ${anno} of artifact ${artName} has been ${changeKind}`;
426
455
  }
427
456
 
428
457
  const sqlSnippetAnnos = [ '@sql.prepend', '@sql.append' ];
@@ -624,8 +653,8 @@ function toSqlDdl(csn, options) {
624
653
  if (options.toSql.dialect === 'hana')
625
654
  renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);
626
655
 
627
- if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
628
- result += ` COMMENT '${getHanaComment(art)}'`;
656
+ if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
657
+ result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
629
658
 
630
659
  if (back)
631
660
  result += back;
@@ -738,8 +767,8 @@ function toSqlDdl(csn, options) {
738
767
  if (back !== '') // Needs to be rendered before the COMMENT
739
768
  result += back;
740
769
 
741
- if (options.toSql.dialect === 'hana' && hasHanaComment(elm, options))
742
- result += ` COMMENT '${getHanaComment(elm)}'`;
770
+ if (options.sqlDialect === 'hana' && hasHanaComment(elm, options))
771
+ result += ` COMMENT ${renderStringForSql(getHanaComment(elm), options.sqlDialect)}`;
743
772
 
744
773
  return result;
745
774
  }
@@ -1089,13 +1118,14 @@ function toSqlDdl(csn, options) {
1089
1118
  * Return the resulting source string (one line per column item, no CR).
1090
1119
  *
1091
1120
  * @param {object} col Column to render
1121
+ * @param {CSN.Elements} elements of leading or subquery
1092
1122
  * @param {object} env Render environment
1093
1123
  * @returns {string} Rendered column
1094
1124
  */
1095
- function renderViewColumn(col, env) {
1125
+ function renderViewColumn(col, elements, env) {
1096
1126
  let result = '';
1097
1127
  const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func;
1098
- if (leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
1128
+ if (leaf && elements[leaf] && elements[leaf].virtual) {
1099
1129
  if (isDeprecatedEnabled(options, 'renderVirtualElements'))
1100
1130
  // render a virtual column 'null as <alias>'
1101
1131
  result += `${env.indent}NULL AS ${quoteSqlId(col.as || leaf)}`;
@@ -1124,11 +1154,11 @@ function toSqlDdl(csn, options) {
1124
1154
  definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art && art.$location, artifactName);
1125
1155
  let result = `VIEW ${viewName}`;
1126
1156
 
1127
- if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
1128
- result += ` COMMENT '${getHanaComment(art)}'`;
1157
+ if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
1158
+ result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
1129
1159
 
1130
1160
  result += renderParameterDefinitions(artifactName, art.params);
1131
- result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env)}`;
1161
+ result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env, art.elements)}`;
1132
1162
 
1133
1163
  const childEnv = increaseIndent(env);
1134
1164
  const associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
@@ -1188,9 +1218,10 @@ function toSqlDdl(csn, options) {
1188
1218
  * @param {string} artifactName Artifact containing the query
1189
1219
  * @param {CSN.Query} query CSN query
1190
1220
  * @param {object} env Render environment
1221
+ * @param {CSN.Elements} [elements] to override direct query elements - e.g. leading union should win
1191
1222
  * @returns {string} Rendered query
1192
1223
  */
1193
- function renderQuery(artifactName, query, env) {
1224
+ function renderQuery(artifactName, query, env, elements = null) {
1194
1225
  let result = '';
1195
1226
  // Set operator, like UNION, INTERSECT, ...
1196
1227
  if (query.SET) {
@@ -1199,7 +1230,7 @@ function toSqlDdl(csn, options) {
1199
1230
  // Wrap each query in the SET in parentheses that
1200
1231
  // - is a SET itself (to preserve precedence between the different SET operations),
1201
1232
  // - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY)
1202
- const queryString = renderQuery(artifactName, arg, env);
1233
+ const queryString = renderQuery(artifactName, arg, env, elements || query.SET.elements);
1203
1234
  return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;
1204
1235
  })
1205
1236
  .join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
@@ -1228,7 +1259,7 @@ function toSqlDdl(csn, options) {
1228
1259
  // FIXME: We probably also need to consider `excluding` here ?
1229
1260
  result += `\n${(select.columns || [ '*' ])
1230
1261
  .filter(col => !(select.mixin || Object.create(null))[firstPathStepId(col.ref)]) // No mixin columns
1231
- .map(col => renderViewColumn(col, childEnv))
1262
+ .map(col => renderViewColumn(col, elements || select.elements, childEnv))
1232
1263
  .filter(s => s !== '')
1233
1264
  .join(',\n')}\n`;
1234
1265
  result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`;
@@ -1403,171 +1434,80 @@ function toSqlDdl(csn, options) {
1403
1434
  return params.length === 0 ? '' : `(${params.join(', ')})`;
1404
1435
  }
1405
1436
 
1406
- /**
1407
- * Render an expression (including paths and values) or condition 'x'.
1408
- * (no trailing LF, don't indent if inline)
1409
- *
1410
- * @todo Reuse this with toCdl
1411
- * @param {Array|object|string} expr Expression to render
1412
- * @param {object} env Render environment
1413
- * @param {boolean} [inline=true] Whether to render the expression inline
1414
- * @param {boolean} [nestedExpr=false] Whether to treat the expression as nested
1415
- * @param {boolean} [alwaysRenderCast=false] Whether to _always_ render SQL-style casts, even if `nestedExpr === false`.
1416
- * Note: This is a hack for casts() inside groupBy.
1417
- * @returns {string} Rendered expression
1418
- */
1419
- function renderExpr(expr, env, inline = true, nestedExpr = false, alwaysRenderCast = false) {
1420
- // Compound expression
1421
- if (Array.isArray(expr)) {
1422
- const tokens = expr.map(item => renderExpr(item, env, inline, nestedExpr));
1423
- return beautifyExprArray(tokens);
1424
- }
1425
- else if (typeof expr === 'object' && expr !== null) {
1426
- if ((nestedExpr || alwaysRenderCast) && expr.cast && expr.cast.type)
1427
- return renderExplicitTypeCast(expr, renderExprObject(expr));
1428
- return renderExprObject(expr);
1429
- }
1430
- // Not a literal value but part of an operator, function etc - just leave as it is
1431
- // FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
1437
+ function renderExpressionLiteral(x) {
1438
+ // Literal value, possibly with explicit 'literal' property
1439
+ switch (x.literal || typeof x.val) {
1440
+ case 'number':
1441
+ case 'boolean':
1442
+ case 'null':
1443
+ // 17.42, NULL, TRUE
1444
+ return String(x.val).toUpperCase();
1445
+ case 'x':
1446
+ // x'f000'
1447
+ return `${x.literal}'${x.val}'`;
1448
+ case 'date':
1449
+ case 'time':
1450
+ case 'timestamp':
1451
+ if (options.toSql.dialect === 'sqlite') {
1452
+ // simple string literal '2017-11-02'
1453
+ return `'${x.val}'`;
1454
+ }
1455
+ // date'2017-11-02'
1456
+ return `${x.literal}'${x.val}'`;
1432
1457
 
1433
- return String(expr).toUpperCase();
1458
+ case 'string':
1459
+ // 'foo', with proper escaping
1460
+ return renderStringForSql(x.val, options.sqlDialect);
1461
+ case 'object':
1462
+ if (x.val === null)
1463
+ return 'NULL';
1434
1464
 
1465
+ // otherwise fall through to
1466
+ default:
1467
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1468
+ }
1469
+ }
1435
1470
 
1436
- /**
1437
- * Various special cases represented as objects
1438
- *
1439
- * @param {object} x Expression
1440
- * @returns {string} String representation of the expression
1441
- */
1442
- function renderExprObject(x) {
1443
- if (x.list) {
1444
- return `(${x.list.map(item => renderExpr(item)).join(', ')})`;
1445
- }
1446
- else if (x.val !== undefined) {
1447
- return renderExpressionLiteral(x);
1448
- }
1449
- // Enum symbol
1450
- else if (x['#']) {
1451
- // #foo
1452
- // TODO: Signal is not covered by tests + better location
1453
- // FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand)
1454
- error(null, x.$location, 'Enum values are not yet supported for conversion to SQL');
1455
- return '';
1456
- }
1457
- // Reference: Array of path steps, possibly preceded by ':'
1458
- else if (x.ref) {
1459
- return renderExpressionRef(x);
1460
- }
1461
- // Function call, possibly with args (use '=>' for named args)
1462
- else if (x.func) {
1463
- const funcName = smartFuncId(prepareIdentifier(x.func), options.toSql.dialect);
1464
- if (x.xpr)
1465
- return renderWindowFunction(funcName, x, env);
1466
- return renderFunc(funcName, x, options.toSql.dialect, a => renderArgs(a, '=>', env, null));
1467
- }
1468
- // Nested expression
1469
- else if (x.xpr) {
1470
- if (nestedExpr && !x.cast)
1471
- return `(${renderExpr(x.xpr, env, inline, true)})`;
1471
+ function renderExpressionRef(x, env) {
1472
+ if (!x.param && !x.global) {
1473
+ const magicReplacement = getVariableReplacement(x.ref, options);
1472
1474
 
1473
- return renderExpr(x.xpr, env, inline, true);
1475
+ if (x.ref[0] === '$user') {
1476
+ if (magicReplacement !== null)
1477
+ return `'${magicReplacement}'`;
1478
+
1479
+ const result = render$user();
1480
+ // Invalid second path step doesn't cause a return
1481
+ if (result)
1482
+ return result;
1474
1483
  }
1475
- // Sub-select
1476
- else if (x.SELECT) {
1477
- // renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
1478
- return `(${renderQuery('<subselect>', x, increaseIndent(env))})`;
1484
+ else if (x.ref[0] === '$at') {
1485
+ const result = render$at();
1486
+ // Invalid second path step doesn't cause a return
1487
+ if (result)
1488
+ return result;
1479
1489
  }
1480
- else if (x.SET) {
1481
- // renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
1482
- return `${renderQuery('<union>', x, increaseIndent(env))}`;
1490
+ else if (x.ref[0] === '$session' && magicReplacement !== null) {
1491
+ return `'${magicReplacement}'`;
1483
1492
  }
1484
-
1485
- throw new ModelError(`Unknown expression: ${JSON.stringify(x)}`);
1486
- }
1487
-
1488
- function renderWindowFunction(funcName, node, fctEnv) {
1489
- const suffix = node.xpr[0]; // OVER
1490
- let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)})`;
1491
- r += ` ${suffix} (${renderExpr(node.xpr.slice(1), fctEnv)})`; // do not pass suffix in renderExpr
1492
- return r;
1493
1493
  }
1494
-
1495
- function renderExpressionLiteral(x) {
1496
- // Literal value, possibly with explicit 'literal' property
1497
- switch (x.literal || typeof x.val) {
1498
- case 'number':
1499
- case 'boolean':
1500
- case 'null':
1501
- // 17.42, NULL, TRUE
1502
- return String(x.val).toUpperCase();
1503
- case 'x':
1504
- // x'f000'
1505
- return `${x.literal}'${x.val}'`;
1506
- case 'date':
1507
- case 'time':
1508
- case 'timestamp':
1509
- if (options.toSql.dialect === 'sqlite') {
1510
- // simple string literal '2017-11-02'
1511
- return `'${x.val}'`;
1512
- }
1513
- // date'2017-11-02'
1514
- return `${x.literal}'${x.val}'`;
1515
-
1516
- case 'string':
1517
- // 'foo', with proper escaping
1518
- return `'${x.val.replace(/'/g, '\'\'')}'`;
1519
- case 'object':
1520
- if (x.val === null)
1521
- return 'NULL';
1522
-
1523
- // otherwise fall through to
1524
- default:
1525
- throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1526
- }
1494
+ // FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
1495
+ // assume that it was not if the path has length 2 (
1496
+ if (firstPathStepId(x.ref) === '$parameters' && x.ref.length === 2) {
1497
+ // Parameters must be uppercased and unquoted in SQL
1498
+ return `:${x.ref[1].toUpperCase()}`;
1527
1499
  }
1500
+ if (x.param)
1501
+ return `:${x.ref[0].toUpperCase()}`;
1528
1502
 
1529
- function renderExpressionRef(x) {
1530
- if (!x.param && !x.global) {
1531
- const magicReplacement = getVariableReplacement(x.ref, options);
1532
-
1533
- if (x.ref[0] === '$user') {
1534
- if (magicReplacement !== null)
1535
- return `'${magicReplacement}'`;
1536
-
1537
- const result = render$user(x);
1538
- // Invalid second path step doesn't cause a return
1539
- if (result)
1540
- return result;
1541
- }
1542
- else if (x.ref[0] === '$at') {
1543
- const result = render$at(x);
1544
- // Invalid second path step doesn't cause a return
1545
- if (result)
1546
- return result;
1547
- }
1548
- else if (x.ref[0] === '$session' && magicReplacement !== null) {
1549
- return `'${magicReplacement}'`;
1550
- }
1551
- }
1552
- // FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
1553
- // assume that it was not if the path has length 2 (
1554
- if (firstPathStepId(x.ref) === '$parameters' && x.ref.length === 2) {
1555
- // Parameters must be uppercased and unquoted in SQL
1556
- return `:${x.ref[1].toUpperCase()}`;
1557
- }
1558
- if (x.param)
1559
- return `:${x.ref[0].toUpperCase()}`;
1560
-
1561
- return x.ref.map(renderPathStep)
1562
- .filter(s => s !== '')
1563
- .join('.');
1564
- }
1503
+ return x.ref.map(renderPathStep)
1504
+ .filter(s => s !== '')
1505
+ .join('.');
1565
1506
 
1566
1507
  /**
1567
- * @param {object} x
1568
1508
  * @returns {string|null} Null in case of an invalid second path step
1569
1509
  */
1570
- function render$user(x) {
1510
+ function render$user() {
1571
1511
  // FIXME: this is all not enough: we might need an explicit select item alias
1572
1512
  if (x.ref[1] === 'id') {
1573
1513
  // Keep the old-style for compatibilty with magicVars.id - instead of magicVars.user.id...
@@ -1595,18 +1535,17 @@ function toSqlDdl(csn, options) {
1595
1535
  }
1596
1536
  /**
1597
1537
  * For a given reference starting with $at, render a 'current_timestamp' literal for plain.
1598
- * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1599
- *
1600
- *
1601
- * For sqlite, we render the string-format-time (strftime) function.
1602
- * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1603
- * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1604
- * --> Therefore the comparison in the temporal where clause doesn't work properly.
1605
- *
1606
- * @param {object} x
1607
- * @returns {string|null} Null in case of an invalid second path step
1608
- */
1609
- function render$at(x) {
1538
+ * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1539
+ *
1540
+ *
1541
+ * For sqlite, we render the string-format-time (strftime) function.
1542
+ * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1543
+ * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1544
+ * --> Therefore the comparison in the temporal where clause doesn't work properly.
1545
+ *
1546
+ * @returns {string|null} Null in case of an invalid second path step
1547
+ */
1548
+ function render$at() {
1610
1549
  if (x.ref[1] === 'from') {
1611
1550
  switch (options.toSql.dialect) {
1612
1551
  case 'sqlite': {
@@ -1640,18 +1579,6 @@ function toSqlDdl(csn, options) {
1640
1579
  return null;
1641
1580
  }
1642
1581
 
1643
- /**
1644
- * Renders an explicit `cast()` inside an 'xpr'.
1645
- *
1646
- * @param {object} x Expression with cast
1647
- * @param {string} value Value to cast
1648
- * @returns {string} CAST statement
1649
- */
1650
- function renderExplicitTypeCast(x, value) {
1651
- const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast);
1652
- return `CAST(${value} AS ${typeRef})`;
1653
- }
1654
-
1655
1582
  /**
1656
1583
  * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1657
1584
  *
@@ -1709,6 +1636,13 @@ function toSqlDdl(csn, options) {
1709
1636
  }
1710
1637
  }
1711
1638
 
1639
+ function renderWindowFunction(funcName, node, fctEnv) {
1640
+ const suffix = node.xpr[0]; // OVER
1641
+ let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)})`;
1642
+ r += ` ${suffix} (${renderExpr(node.xpr.slice(1), fctEnv)})`; // do not pass suffix in renderExpr
1643
+ return r;
1644
+ }
1645
+
1712
1646
  /**
1713
1647
  * Returns a copy of 'env' with increased indentation
1714
1648
  *
@@ -1720,6 +1654,54 @@ function toSqlDdl(csn, options) {
1720
1654
  }
1721
1655
  }
1722
1656
 
1657
+ /**
1658
+ * Render the given string for SQL databases.
1659
+ *
1660
+ * @param {string} str
1661
+ * @param {string} sqlDialect
1662
+ * @return {string}
1663
+ */
1664
+ function renderStringForSql(str, sqlDialect) {
1665
+ if (sqlDialect === 'hana' || sqlDialect === 'sqlite') {
1666
+ // SQLite
1667
+ // ======
1668
+ // SQLite's tokenizer available at
1669
+ // <https://www.sqlite.org/src/file?name=src/tokenize.c>.
1670
+ //
1671
+ // Note that NUL may have side effects, as explained on
1672
+ // <https://sqlite.org/nulinstr.html>.
1673
+ //
1674
+ //
1675
+ // HANA
1676
+ // ====
1677
+ // Respects the specification available at
1678
+ // <https://help.sap.com/doc/9b40bf74f8644b898fb07dabdd2a36ad/2.0.04/en-US/SAP_HANA_SQL_Reference_Guide_en.pdf>.
1679
+ //
1680
+ // <string_literal> ::= <single_quote>[<any_character>...]<single_quote>
1681
+ // <single_quote> ::= '
1682
+ //
1683
+ // and
1684
+ // > # Quotation marks
1685
+ // > Single quotation marks are used to delimit string literals.
1686
+ // > A single quotation mark itself can be represented using two single quotation marks.
1687
+ str = str.replace(/'/g, '\'\'')
1688
+ .replace(/\u{0}/ug, '\' || CHAR(0) || \'');
1689
+ }
1690
+ else {
1691
+ // Generic SQL databases
1692
+ // =====================
1693
+ // While escaping NUL may be useful to avoid the SQL file being identified as binary,
1694
+ // we can't escape it using `CHAR(0)`. This function is not available on e.g. PostgreSQL.
1695
+ // On top of this, PostgreSQL also has this limitation:
1696
+ // > chr(int) | text | Character with the given code. For UTF8 the argument is treated as a Unicode code point.
1697
+ // > | | For other multibyte encodings the argument must designate an ASCII character. The NULL (0)
1698
+ // > | | character is not allowed because text data types cannot store such bytes.
1699
+ // - <https://www.postgresql.org/docs/9.1/functions-string.html>
1700
+ str = str.replace(/'/g, '\'\'');
1701
+ }
1702
+ return `'${str}'`;
1703
+ }
1704
+
1723
1705
  module.exports = {
1724
1706
  toSqlDdl,
1725
1707
  };