@sap/cds-compiler 2.12.0 → 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 (128) hide show
  1. package/CHANGELOG.md +221 -15
  2. package/bin/cdsc.js +125 -50
  3. package/bin/cdsse.js +2 -2
  4. package/doc/CHANGELOG_BETA.md +13 -6
  5. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  6. package/doc/NameResolution.md +21 -16
  7. package/lib/api/main.js +47 -84
  8. package/lib/api/options.js +5 -6
  9. package/lib/api/validate.js +6 -11
  10. package/lib/backends.js +15 -23
  11. package/lib/base/dictionaries.js +0 -8
  12. package/lib/base/error.js +26 -0
  13. package/lib/base/keywords.js +7 -17
  14. package/lib/base/location.js +9 -4
  15. package/lib/base/message-registry.js +114 -18
  16. package/lib/base/messages.js +101 -90
  17. package/lib/base/model.js +2 -63
  18. package/lib/base/optionProcessorHelper.js +177 -123
  19. package/lib/checks/annotationsOData.js +12 -33
  20. package/lib/checks/arrayOfs.js +1 -34
  21. package/lib/checks/cdsPersistence.js +2 -1
  22. package/lib/checks/enricher.js +17 -1
  23. package/lib/checks/invalidTarget.js +3 -1
  24. package/lib/checks/managedWithoutKeys.js +3 -1
  25. package/lib/checks/selectItems.js +4 -4
  26. package/lib/checks/sql-snippets.js +27 -26
  27. package/lib/checks/types.js +1 -1
  28. package/lib/checks/validator.js +6 -11
  29. package/lib/compiler/assert-consistency.js +6 -3
  30. package/lib/compiler/base.js +1 -0
  31. package/lib/compiler/builtins.js +19 -6
  32. package/lib/compiler/checks.js +23 -60
  33. package/lib/compiler/cycle-detector.js +1 -1
  34. package/lib/compiler/define.js +1151 -0
  35. package/lib/compiler/extend.js +1000 -0
  36. package/lib/compiler/finalize-parse-cdl.js +237 -0
  37. package/lib/compiler/index.js +107 -39
  38. package/lib/compiler/kick-start.js +190 -0
  39. package/lib/compiler/moduleLayers.js +4 -4
  40. package/lib/compiler/populate.js +1227 -0
  41. package/lib/compiler/propagator.js +114 -46
  42. package/lib/compiler/resolve.js +1521 -0
  43. package/lib/compiler/shared.js +126 -65
  44. package/lib/compiler/tweak-assocs.js +535 -0
  45. package/lib/compiler/utils.js +197 -33
  46. package/lib/edm/.eslintrc.json +5 -0
  47. package/lib/edm/annotations/genericTranslation.js +38 -24
  48. package/lib/edm/annotations/preprocessAnnotations.js +2 -2
  49. package/lib/edm/csn2edm.js +219 -100
  50. package/lib/edm/edm.js +302 -230
  51. package/lib/edm/edmPreprocessor.js +554 -419
  52. package/lib/edm/edmUtils.js +138 -44
  53. package/lib/gen/Dictionary.json +100 -19
  54. package/lib/gen/language.checksum +1 -1
  55. package/lib/gen/language.interp +11 -1
  56. package/lib/gen/language.tokens +86 -83
  57. package/lib/gen/languageLexer.interp +10 -1
  58. package/lib/gen/languageLexer.js +860 -833
  59. package/lib/gen/languageLexer.tokens +78 -75
  60. package/lib/gen/languageParser.js +5765 -4480
  61. package/lib/json/csnVersion.js +10 -11
  62. package/lib/json/from-csn.js +15 -3
  63. package/lib/json/to-csn.js +126 -68
  64. package/lib/language/docCommentParser.js +4 -4
  65. package/lib/language/genericAntlrParser.js +123 -5
  66. package/lib/language/language.g4 +355 -156
  67. package/lib/language/multiLineStringParser.js +5 -5
  68. package/lib/main.d.ts +486 -59
  69. package/lib/main.js +41 -9
  70. package/lib/model/api.js +3 -1
  71. package/lib/model/csnRefs.js +252 -156
  72. package/lib/model/csnUtils.js +384 -297
  73. package/lib/model/enrichCsn.js +71 -29
  74. package/lib/model/revealInternalProperties.js +29 -8
  75. package/lib/model/sortViews.js +2 -1
  76. package/lib/modelCompare/compare.js +23 -18
  77. package/lib/optionProcessor.js +63 -26
  78. package/lib/render/manageConstraints.js +35 -32
  79. package/lib/render/toCdl.js +897 -947
  80. package/lib/render/toHdbcds.js +205 -257
  81. package/lib/render/toSql.js +264 -225
  82. package/lib/render/utils/common.js +136 -25
  83. package/lib/render/utils/sql.js +4 -3
  84. package/lib/render/utils/stringEscapes.js +111 -0
  85. package/lib/sql-identifier.js +1 -1
  86. package/lib/transform/.eslintrc.json +5 -0
  87. package/lib/transform/db/.eslintrc.json +3 -1
  88. package/lib/transform/db/applyTransformations.js +35 -12
  89. package/lib/transform/db/assertUnique.js +1 -1
  90. package/lib/transform/db/associations.js +104 -306
  91. package/lib/transform/db/cdsPersistence.js +2 -2
  92. package/lib/transform/db/constraints.js +58 -53
  93. package/lib/transform/db/expansion.js +60 -33
  94. package/lib/transform/db/flattening.js +582 -104
  95. package/lib/transform/db/groupByOrderBy.js +3 -1
  96. package/lib/transform/db/transformExists.js +66 -13
  97. package/lib/transform/db/views.js +11 -7
  98. package/lib/transform/draft/.eslintrc.json +38 -0
  99. package/lib/transform/{db/draft.js → draft/db.js} +6 -5
  100. package/lib/transform/draft/odata.js +227 -0
  101. package/lib/transform/forHanaNew.js +109 -208
  102. package/lib/transform/forOdataNew.js +59 -212
  103. package/lib/transform/localized.js +46 -26
  104. package/lib/transform/odata/toFinalBaseType.js +85 -11
  105. package/lib/transform/odata/typesExposure.js +147 -199
  106. package/lib/transform/odata/utils.js +2 -2
  107. package/lib/transform/transformUtilsNew.js +44 -33
  108. package/lib/transform/translateAssocsToJoins.js +3 -20
  109. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  110. package/lib/transform/universalCsn/coreComputed.js +172 -0
  111. package/lib/transform/universalCsn/universalCsnEnricher.js +737 -0
  112. package/lib/transform/universalCsn/utils.js +63 -0
  113. package/lib/utils/moduleResolve.js +13 -6
  114. package/lib/utils/objectUtils.js +30 -0
  115. package/package.json +1 -1
  116. package/share/messages/README.md +26 -0
  117. package/share/messages/message-explanations.json +2 -1
  118. package/share/messages/syntax-expected-integer.md +37 -0
  119. package/lib/compiler/definer.js +0 -2361
  120. package/lib/compiler/resolver.js +0 -3079
  121. package/lib/transform/odata/attachPath.js +0 -96
  122. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  123. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  124. package/lib/transform/odata/referenceFlattener.js +0 -290
  125. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  126. package/lib/transform/odata/structuralPath.js +0 -72
  127. package/lib/transform/odata/structureFlattener.js +0 -171
  128. package/lib/transform/universalCsnEnricher.js +0 -237
@@ -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,
@@ -21,6 +21,7 @@ const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
21
21
  const { smartFuncId } = require('../sql-identifier');
22
22
  const { sortCsn } = require('../json/to-csn');
23
23
  const { manageConstraints } = require('./manageConstraints');
24
+ const { ModelError } = require('../base/error');
24
25
 
25
26
 
26
27
  /**
@@ -76,9 +77,37 @@ const { manageConstraints } = require('./manageConstraints');
76
77
  function toSqlDdl(csn, options) {
77
78
  timetrace.start('SQL rendering');
78
79
  const {
79
- error, warning, info, throwWithError,
80
+ error, warning, info, throwWithAnyError,
80
81
  } = makeMessageFunction(csn, options, 'to.sql');
81
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
+ });
82
111
 
83
112
  // Utils to render SQL statements.
84
113
  const render = {
@@ -88,7 +117,8 @@ function toSqlDdl(csn, options) {
88
117
  */
89
118
  addColumns: {
90
119
  fromElementStrings(tableName, eltStrings) {
91
- return [ `ALTER TABLE ${tableName} ADD (${eltStrings.join(', ')});` ];
120
+ const elts = options.sqlDialect === 'hana' ? `(${eltStrings.join(', ')})` : `${eltStrings.join(', ')}`;
121
+ return [ `ALTER TABLE ${tableName} ADD ${elts};` ];
92
122
  },
93
123
  fromElementsObj(artifactName, tableName, elementsObj, env, duplicateChecker) {
94
124
  // Only extend with 'ADD' for elements/associations
@@ -170,7 +200,13 @@ function toSqlDdl(csn, options) {
170
200
  Render comment string.
171
201
  */
172
202
  comment(comment) {
173
- return comment && `'${comment.replace(/'/g, '\'\'')}'` || 'NULL';
203
+ return comment && renderStringForSql(comment, options.sqlDialect) || 'NULL';
204
+ },
205
+ /*
206
+ Alter SQL snippet for entity.
207
+ */
208
+ alterEntitySqlSnippet(tableName, snippet) {
209
+ return [ `ALTER TABLE ${tableName} ${snippet};` ];
174
210
  },
175
211
  /*
176
212
  Concatenate multiple statements which are to be treated as one by the API caller.
@@ -226,7 +262,7 @@ function toSqlDdl(csn, options) {
226
262
  // Render each artifact extension
227
263
  // Only HANA SQL is currently supported.
228
264
  // Note that extensions may contain new elements referenced in migrations, thus should be compiled first.
229
- if (csn.extensions && options.toSql.dialect === 'hana') {
265
+ if (csn.extensions && (options.toSql.dialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
230
266
  for (const extension of options && options.testMode ? sortCsn(csn.extensions) : csn.extensions) {
231
267
  if (extension.extend) {
232
268
  const artifactName = extension.extend;
@@ -239,7 +275,7 @@ function toSqlDdl(csn, options) {
239
275
 
240
276
  // Render each artifact change
241
277
  // Only HANA SQL is currently supported.
242
- if (csn.migrations && options.toSql.dialect === 'hana') {
278
+ if (csn.migrations && (options.toSql.dialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
243
279
  for (const migration of options && options.testMode ? sortCsn(csn.migrations) : csn.migrations) {
244
280
  if (migration.migrate) {
245
281
  const artifactName = migration.migrate;
@@ -256,7 +292,7 @@ function toSqlDdl(csn, options) {
256
292
  deletionsDuplicateChecker.check(error);
257
293
 
258
294
  // Throw exception in case of errors
259
- throwWithError();
295
+ throwWithAnyError();
260
296
 
261
297
  // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src === 'sql'
262
298
  // (relying on the order of dictionaries above)
@@ -284,8 +320,8 @@ function toSqlDdl(csn, options) {
284
320
  delete mainResultObj[hdbKind];
285
321
  }
286
322
 
287
- // add `ALTER TABLE ADD CONSTRAINT` statements if requested
288
- if (options.sqlDialect !== 'sqlite' && options.constraintsAsAlter) {
323
+ // add `ALTER TABLE ADD CONSTRAINT` statements per default for `to.sql` w/ dialect `hana`
324
+ if (!options.constraintsInCreateTable && options.src === 'sql' && options.sqlDialect === 'hana') {
289
325
  const alterStmts = manageConstraints(csn, options);
290
326
 
291
327
  for ( const constraintName of Object.keys(alterStmts))
@@ -318,7 +354,6 @@ function toSqlDdl(csn, options) {
318
354
 
319
355
  switch (art.kind) {
320
356
  case 'entity':
321
- case 'view':
322
357
  if (getNormalizedQuery(art).query) {
323
358
  const result = renderView(artifactName, art, env);
324
359
  if (result)
@@ -340,7 +375,7 @@ function toSqlDdl(csn, options) {
340
375
  // Ignore: not SQL-relevant
341
376
  return;
342
377
  default:
343
- throw new Error(`Unknown artifact kind: ${art.kind}`);
378
+ throw new ModelError(`Unknown artifact kind: ${art.kind}`);
344
379
  }
345
380
  }
346
381
 
@@ -361,7 +396,7 @@ function toSqlDdl(csn, options) {
361
396
  renderExtendInto(artifactName, artifact.elements, ext.elements, resultObj, env, extensionsDuplicateChecker);
362
397
 
363
398
  if (!artifactName)
364
- throw new Error(`Undefined artifact name: ${artifactName}`);
399
+ throw new ModelError(`Undefined artifact name: ${artifactName}`);
365
400
  }
366
401
 
367
402
  // Render an artifact deletion into the appropriate dictionary of 'resultObj'.
@@ -401,12 +436,26 @@ function toSqlDdl(csn, options) {
401
436
  ? renderAssociationElement(eltName, defVariant, env)
402
437
  : renderElement(artifactName, eltName, defVariant, null, null, env);
403
438
  }
404
- function getEltStrNoProp(defVariant, prop, eltName) {
405
- const defNoProp = Object.assign({}, defVariant);
406
- delete defNoProp[prop];
407
- return getEltStr(defNoProp, eltName);
439
+ function getEltStrNoProps(defVariant, eltName, ...props) {
440
+ const defNoProps = Object.assign({}, defVariant);
441
+ for (const prop of props)
442
+ delete defNoProps[prop];
443
+ return getEltStr(defNoProps, eltName);
444
+ }
445
+ function oldAnnoChangedIncompatibly(defOld, defNew) {
446
+ return typeof defOld === 'string' && defOld.trim().length && !(typeof defNew === 'string' && defNew.trim().startsWith(`${defOld.trim()} `));
447
+ }
448
+ function getUnknownSqlReason(anno, artName, defOld, defNew, eltName) {
449
+ const changeKind = defNew === undefined
450
+ ? `removed (previous value: ${JSON.stringify(defOld)})`
451
+ : `changed from ${JSON.stringify(defOld)} to ${JSON.stringify(defNew)}`;
452
+ return eltName
453
+ ? `annotation ${anno} of element ${artName}:${eltName} has been ${changeKind}`
454
+ : `annotation ${anno} of artifact ${artName} has been ${changeKind}`;
408
455
  }
409
456
 
457
+ const sqlSnippetAnnos = [ '@sql.prepend', '@sql.append' ];
458
+
410
459
  const tableName = renderArtifactName(artifactName);
411
460
 
412
461
  // Change entity properties
@@ -416,6 +465,15 @@ function toSqlDdl(csn, options) {
416
465
  const alterComment = render.alterEntityComment(tableName, def.new);
417
466
  addMigration(resultObj, artifactName, false, alterComment);
418
467
  }
468
+ else if (sqlSnippetAnnos.includes(prop)) { // NOTE: @sql.replace may be supported in the future
469
+ if (oldAnnoChangedIncompatibly(def.old, def.new)) {
470
+ // anno was previously set and current change is not simply an appendix → previous anno would have to be reverted → unknown SQL
471
+ addMigration(resultObj, artifactName, false, null, getUnknownSqlReason(prop, artifactName, def.old, def.new));
472
+ }
473
+ else {
474
+ addMigration(resultObj, artifactName, false, render.alterEntitySqlSnippet(tableName, def.new));
475
+ }
476
+ }
419
477
  }
420
478
  }
421
479
 
@@ -450,9 +508,28 @@ function toSqlDdl(csn, options) {
450
508
  if (eltStrNew === eltStrOld)
451
509
  return; // Prevent spurious migrations, where the column DDL does not change.
452
510
 
511
+ const annosIncompat = [];
512
+ sqlSnippetAnnos
513
+ .filter(anno => def.old[anno] !== def.new[anno])
514
+ .forEach((anno) => { // NOTE: @sql.replace may be supported in the future
515
+ if (oldAnnoChangedIncompatibly(def.old[anno], def.new[anno])) {
516
+ annosIncompat.push(anno);
517
+ // anno was previously set and current change is not simply an appendix → previous anno would have to be reverted → unknown SQL
518
+ addMigration(resultObj, artifactName, false, null, getUnknownSqlReason(anno, artifactName, def.old[anno], def.new[anno], eltName));
519
+ }
520
+ });
521
+
522
+ if (annosIncompat.length) {
523
+ const eltStrOldNoAnnos = getEltStrNoProps(def.old, eltName, ...annosIncompat);
524
+ const eltStrNewNoAnnos = getEltStrNoProps(def.new, eltName, ...annosIncompat);
525
+ if (eltStrOldNoAnnos === eltStrNewNoAnnos) { // only incompatibly-changed annos were modified
526
+ continue;
527
+ }
528
+ }
529
+
453
530
  if (def.old.doc !== def.new.doc) {
454
- const eltStrOldNoDoc = getEltStrNoProp(def.old, 'doc', eltName);
455
- const eltStrNewNoDoc = getEltStrNoProp(def.new, 'doc', eltName);
531
+ const eltStrOldNoDoc = getEltStrNoProps(def.old, eltName, 'doc');
532
+ const eltStrNewNoDoc = getEltStrNoProps(def.new, eltName, 'doc');
456
533
  if (eltStrOldNoDoc === eltStrNewNoDoc) { // only `doc` changed
457
534
  const alterComment = render.alterColumnComment(tableName, sqlId, def.new.doc);
458
535
  addMigration(resultObj, artifactName, false, alterComment);
@@ -501,7 +578,7 @@ function toSqlDdl(csn, options) {
501
578
  // Explicitly specified
502
579
  result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
503
580
  }
504
- else {
581
+ else if (!front) {
505
582
  // in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
506
583
  result += 'COLUMN ';
507
584
  }
@@ -524,7 +601,8 @@ function toSqlDdl(csn, options) {
524
601
  if (primaryKeys !== '')
525
602
  result += `,\n${childEnv.indent}${primaryKeys}`;
526
603
 
527
- const constraintsAsAlter = options.constraintsAsAlter && options.sqlDialect !== 'sqlite';
604
+ // for `to.sql` w/ dialect `hana` the constraints will be part of the
605
+ const constraintsAsAlter = !options.constraintsInCreateTable && options.src === 'sql' && options.sqlDialect === 'hana';
528
606
  if ( !constraintsAsAlter && art.$tableConstraints && art.$tableConstraints.referential) {
529
607
  const renderReferentialConstraintsAsHdbconstraint = options.toSql.src === 'hdi';
530
608
  const referentialConstraints = {};
@@ -575,8 +653,8 @@ function toSqlDdl(csn, options) {
575
653
  if (options.toSql.dialect === 'hana')
576
654
  renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);
577
655
 
578
- if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
579
- result += ` COMMENT '${getHanaComment(art)}'`;
656
+ if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
657
+ result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
580
658
 
581
659
  if (back)
582
660
  result += back;
@@ -612,10 +690,15 @@ function toSqlDdl(csn, options) {
612
690
  }
613
691
  }
614
692
 
615
- function addMigration(resultObj, artifactName, drop, sqlArray) {
693
+ function addMigration(resultObj, artifactName, drop, sqlArray, description) {
616
694
  if (!(artifactName in resultObj.migrations))
617
695
  resultObj.migrations[artifactName] = [];
618
696
 
697
+ if (!sqlArray) {
698
+ if (description)
699
+ resultObj.migrations[artifactName].push({ description });
700
+ return;
701
+ }
619
702
  const migrations = sqlArray.map(migrationSql => ({ drop, sql: migrationSql }));
620
703
  resultObj.migrations[artifactName].push(...migrations);
621
704
  }
@@ -678,15 +761,15 @@ function toSqlDdl(csn, options) {
678
761
  if (fzindex && options.toSql.dialect === 'hana')
679
762
  result += ` ${renderExpr(fzindex, env)}`;
680
763
 
681
- if (options.toSql.dialect === 'hana' && hasHanaComment(elm, options))
682
- result += ` COMMENT '${getHanaComment(elm)}'`;
683
-
684
764
  // (table) elements can only have a @sql.append
685
765
  const { back } = getSqlSnippets(options, elm);
686
766
 
687
- if (back !== '')
767
+ if (back !== '') // Needs to be rendered before the COMMENT
688
768
  result += back;
689
769
 
770
+ if (options.sqlDialect === 'hana' && hasHanaComment(elm, options))
771
+ result += ` COMMENT ${renderStringForSql(getHanaComment(elm), options.sqlDialect)}`;
772
+
690
773
  return result;
691
774
  }
692
775
 
@@ -751,7 +834,7 @@ function toSqlDdl(csn, options) {
751
834
  // This also affects renderIndexes
752
835
  tc = tc.hana;
753
836
  if (!tc)
754
- throw new Error('Expecting a HANA technical configuration');
837
+ throw new ModelError('Expecting a HANA technical configuration');
755
838
 
756
839
  if (tc.tableSuffix) {
757
840
  // Although we could just render the whole bandwurm as one stream of tokens, the
@@ -820,7 +903,7 @@ function toSqlDdl(csn, options) {
820
903
  const i = index.indexOf('index');
821
904
  const j = index.indexOf('(');
822
905
  if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2)
823
- throw new Error(`Unexpected form of index: "${index}"`);
906
+ throw new ModelError(`Unexpected form of index: "${index}"`);
824
907
 
825
908
  let indexName = renderArtifactName(`${artifactName}.${index[i + 1].ref}`);
826
909
  if (options.toSql.names === 'plain')
@@ -876,7 +959,7 @@ function toSqlDdl(csn, options) {
876
959
 
877
960
  // Sanity check
878
961
  if (!source.ref)
879
- throw new Error(`Expecting ref in ${JSON.stringify(source)}`);
962
+ throw new ModelError(`Expecting ref in ${JSON.stringify(source)}`);
880
963
 
881
964
  return renderAbsolutePathWithAlias(artifactName, source, env);
882
965
  }
@@ -918,7 +1001,7 @@ function toSqlDdl(csn, options) {
918
1001
  function renderAbsolutePathWithAlias(artifactName, path, env) {
919
1002
  // This actually can't happen anymore because assoc2joins should have taken care of it
920
1003
  if (path.ref[0].where)
921
- throw new Error(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
1004
+ throw new ModelError(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
922
1005
 
923
1006
 
924
1007
  // SQL needs a ':' after path.ref[0] to separate associations
@@ -956,7 +1039,7 @@ function toSqlDdl(csn, options) {
956
1039
  function renderAbsolutePath(path, sep, env) {
957
1040
  // Sanity checks
958
1041
  if (!path.ref)
959
- throw new Error(`Expecting ref in path: ${JSON.stringify(path)}`);
1042
+ throw new ModelError(`Expecting ref in path: ${JSON.stringify(path)}`);
960
1043
 
961
1044
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
962
1045
  const firstArtifactName = path.ref[0].id || path.ref[0];
@@ -973,7 +1056,7 @@ function toSqlDdl(csn, options) {
973
1056
  if (ref && ref.params) {
974
1057
  result += `(${renderArgs(path.ref[0] || {}, '=>', env, syntax)})`;
975
1058
  }
976
- else if ([ 'udf' ].includes(syntax)) {
1059
+ else if (syntax === 'udf') {
977
1060
  // if syntax is user defined function, render empty argument list
978
1061
  // CV without parameters is called as simple view
979
1062
  result += '()';
@@ -1004,15 +1087,15 @@ function toSqlDdl(csn, options) {
1004
1087
  const args = node.args ? node.args : {};
1005
1088
  // Positional arguments
1006
1089
  if (Array.isArray(args))
1007
- return args.map(arg => renderExpr(arg, env)).join(', ');
1090
+ return args.map(arg => renderExpr(arg, env, true, false, true)).join(', ');
1008
1091
 
1009
1092
  // Named arguments (object/dict)
1010
1093
  else if (typeof args === 'object')
1011
1094
  // if this is a function param which is not a reference to the model, we must not quote it
1012
- return Object.keys(args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
1095
+ return Object.keys(args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env, true, false, true)}`).join(', ');
1013
1096
 
1014
1097
 
1015
- throw new Error(`Unknown args: ${JSON.stringify(args)}`);
1098
+ throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1016
1099
 
1017
1100
 
1018
1101
  /**
@@ -1035,13 +1118,14 @@ function toSqlDdl(csn, options) {
1035
1118
  * Return the resulting source string (one line per column item, no CR).
1036
1119
  *
1037
1120
  * @param {object} col Column to render
1121
+ * @param {CSN.Elements} elements of leading or subquery
1038
1122
  * @param {object} env Render environment
1039
1123
  * @returns {string} Rendered column
1040
1124
  */
1041
- function renderViewColumn(col, env) {
1125
+ function renderViewColumn(col, elements, env) {
1042
1126
  let result = '';
1043
1127
  const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func;
1044
- if (leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
1128
+ if (leaf && elements[leaf] && elements[leaf].virtual) {
1045
1129
  if (isDeprecatedEnabled(options, 'renderVirtualElements'))
1046
1130
  // render a virtual column 'null as <alias>'
1047
1131
  result += `${env.indent}NULL AS ${quoteSqlId(col.as || leaf)}`;
@@ -1070,11 +1154,11 @@ function toSqlDdl(csn, options) {
1070
1154
  definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art && art.$location, artifactName);
1071
1155
  let result = `VIEW ${viewName}`;
1072
1156
 
1073
- if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
1074
- result += ` COMMENT '${getHanaComment(art)}'`;
1157
+ if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
1158
+ result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
1075
1159
 
1076
1160
  result += renderParameterDefinitions(artifactName, art.params);
1077
- result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env)}`;
1161
+ result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env, art.elements)}`;
1078
1162
 
1079
1163
  const childEnv = increaseIndent(env);
1080
1164
  const associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
@@ -1134,9 +1218,10 @@ function toSqlDdl(csn, options) {
1134
1218
  * @param {string} artifactName Artifact containing the query
1135
1219
  * @param {CSN.Query} query CSN query
1136
1220
  * @param {object} env Render environment
1221
+ * @param {CSN.Elements} [elements] to override direct query elements - e.g. leading union should win
1137
1222
  * @returns {string} Rendered query
1138
1223
  */
1139
- function renderQuery(artifactName, query, env) {
1224
+ function renderQuery(artifactName, query, env, elements = null) {
1140
1225
  let result = '';
1141
1226
  // Set operator, like UNION, INTERSECT, ...
1142
1227
  if (query.SET) {
@@ -1145,7 +1230,7 @@ function toSqlDdl(csn, options) {
1145
1230
  // Wrap each query in the SET in parentheses that
1146
1231
  // - is a SET itself (to preserve precedence between the different SET operations),
1147
1232
  // - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY)
1148
- const queryString = renderQuery(artifactName, arg, env);
1233
+ const queryString = renderQuery(artifactName, arg, env, elements || query.SET.elements);
1149
1234
  return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;
1150
1235
  })
1151
1236
  .join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
@@ -1166,7 +1251,7 @@ function toSqlDdl(csn, options) {
1166
1251
  }
1167
1252
  // Otherwise must have a SELECT
1168
1253
  else if (!query.SELECT) {
1169
- throw new Error(`Unexpected query operation ${JSON.stringify(query)}`);
1254
+ throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
1170
1255
  }
1171
1256
  const select = query.SELECT;
1172
1257
  const childEnv = increaseIndent(env);
@@ -1174,7 +1259,7 @@ function toSqlDdl(csn, options) {
1174
1259
  // FIXME: We probably also need to consider `excluding` here ?
1175
1260
  result += `\n${(select.columns || [ '*' ])
1176
1261
  .filter(col => !(select.mixin || Object.create(null))[firstPathStepId(col.ref)]) // No mixin columns
1177
- .map(col => renderViewColumn(col, childEnv))
1262
+ .map(col => renderViewColumn(col, elements || select.elements, childEnv))
1178
1263
  .filter(s => s !== '')
1179
1264
  .join(',\n')}\n`;
1180
1265
  result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`;
@@ -1182,7 +1267,7 @@ function toSqlDdl(csn, options) {
1182
1267
  result += `\n${env.indent}WHERE ${renderExpr(select.where, env, true, true)}`;
1183
1268
 
1184
1269
  if (select.groupBy)
1185
- result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
1270
+ result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env, true, false, true)).join(', ')}`;
1186
1271
 
1187
1272
  if (select.having)
1188
1273
  result += `\n${env.indent}HAVING ${renderExpr(select.having, env, true, true)}`;
@@ -1233,7 +1318,7 @@ function toSqlDdl(csn, options) {
1233
1318
  * @returns {string} Rendered ORDER BY entry
1234
1319
  */
1235
1320
  function renderOrderByEntry(entry, env) {
1236
- let result = renderExpr(entry, env);
1321
+ let result = renderExpr(entry, env, true, false, true);
1237
1322
  if (entry.sort)
1238
1323
  result += ` ${entry.sort.toUpperCase()}`;
1239
1324
 
@@ -1257,7 +1342,7 @@ function toSqlDdl(csn, options) {
1257
1342
  // Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
1258
1343
  if (!elm.type) {
1259
1344
  if (!elm.elements)
1260
- throw new Error(`Missing type of: ${elementName}`);
1345
+ throw new ModelError(`Missing type of: ${elementName}`);
1261
1346
 
1262
1347
  // TODO: Signal is not covered by tests + better location
1263
1348
  error(null, [ 'definitions', artifactName, 'elements', elementName ],
@@ -1280,7 +1365,7 @@ function toSqlDdl(csn, options) {
1280
1365
  result += renderBuiltinType(elm.type);
1281
1366
  }
1282
1367
  else {
1283
- throw new Error(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1368
+ throw new ModelError(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1284
1369
  }
1285
1370
  result += renderTypeParameters(elm);
1286
1371
  return result;
@@ -1309,7 +1394,7 @@ function toSqlDdl(csn, options) {
1309
1394
  * Render the nullability of an element or parameter (can be unset, true, or false)
1310
1395
  *
1311
1396
  * @param {object} obj Object to render for
1312
- * @param {boolean} treatKeyAsNotNull Wether to render KEY as not null
1397
+ * @param {boolean} treatKeyAsNotNull Whether to render KEY as not null
1313
1398
  * @returns {string} NULL/NOT NULL or ''
1314
1399
  */
1315
1400
  function renderNullability(obj, treatKeyAsNotNull = false) {
@@ -1349,168 +1434,80 @@ function toSqlDdl(csn, options) {
1349
1434
  return params.length === 0 ? '' : `(${params.join(', ')})`;
1350
1435
  }
1351
1436
 
1352
- /**
1353
- * Render an expression (including paths and values) or condition 'x'.
1354
- * (no trailing LF, don't indent if inline)
1355
- *
1356
- * @todo Reuse this with toCdl
1357
- * @param {Array|object|string} expr Expression to render
1358
- * @param {object} env Render environment
1359
- * @param {boolean} inline Wether to render the expression inline
1360
- * @param {boolean} nestedExpr Wether to treat the expression as nested
1361
- * @returns {string} Rendered expression
1362
- */
1363
- function renderExpr(expr, env, inline = true, nestedExpr = false) {
1364
- // Compound expression
1365
- if (Array.isArray(expr)) {
1366
- const tokens = expr.map(item => renderExpr(item, env, inline, nestedExpr));
1367
- return beautifyExprArray(tokens);
1368
- }
1369
- else if (typeof expr === 'object' && expr !== null) {
1370
- if (nestedExpr && expr.cast && expr.cast.type)
1371
- return renderExplicitTypeCast(expr, renderExprObject(expr));
1372
- return renderExprObject(expr);
1373
- }
1374
- // Not a literal value but part of an operator, function etc - just leave as it is
1375
- // 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}'`;
1376
1457
 
1377
- 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';
1378
1464
 
1465
+ // otherwise fall through to
1466
+ default:
1467
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1468
+ }
1469
+ }
1379
1470
 
1380
- /**
1381
- * Various special cases represented as objects
1382
- *
1383
- * @returns {string} String representation of the expression
1384
- */
1385
- function renderExprObject(x) {
1386
- if (x.list) {
1387
- return `(${x.list.map(item => renderExpr(item)).join(', ')})`;
1388
- }
1389
- else if (x.val !== undefined) {
1390
- return renderExpressionLiteral(x);
1391
- }
1392
- // Enum symbol
1393
- else if (x['#']) {
1394
- // #foo
1395
- // TODO: Signal is not covered by tests + better location
1396
- // FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand)
1397
- error(null, x.$location, 'Enum values are not yet supported for conversion to SQL');
1398
- return '';
1399
- }
1400
- // Reference: Array of path steps, possibly preceded by ':'
1401
- else if (x.ref) {
1402
- return renderExpressionRef(x);
1403
- }
1404
- // Function call, possibly with args (use '=>' for named args)
1405
- else if (x.func) {
1406
- const funcName = smartFuncId(prepareIdentifier(x.func), options.toSql.dialect);
1407
- if (x.xpr)
1408
- return renderWindowFunction(funcName, x, env);
1409
- return renderFunc(funcName, x, options.toSql.dialect, a => renderArgs(a, '=>', env, null));
1410
- }
1411
- // Nested expression
1412
- else if (x.xpr) {
1413
- if (nestedExpr && !x.cast)
1414
- 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);
1474
+
1475
+ if (x.ref[0] === '$user') {
1476
+ if (magicReplacement !== null)
1477
+ return `'${magicReplacement}'`;
1415
1478
 
1416
- return renderExpr(x.xpr, env, inline, true);
1479
+ const result = render$user();
1480
+ // Invalid second path step doesn't cause a return
1481
+ if (result)
1482
+ return result;
1417
1483
  }
1418
- // Sub-select
1419
- else if (x.SELECT) {
1420
- // renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
1421
- 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;
1422
1489
  }
1423
- else if (x.SET) {
1424
- // renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
1425
- return `${renderQuery('<union>', x, increaseIndent(env))}`;
1490
+ else if (x.ref[0] === '$session' && magicReplacement !== null) {
1491
+ return `'${magicReplacement}'`;
1426
1492
  }
1427
-
1428
- throw new Error(`Unknown expression: ${JSON.stringify(x)}`);
1429
1493
  }
1430
-
1431
- function renderWindowFunction(funcName, node, fctEnv) {
1432
- const suffix = node.xpr[0]; // OVER
1433
- let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)})`;
1434
- r += ` ${suffix} (${renderExpr(node.xpr.slice(1), fctEnv)})`; // do not pass suffix in renderExpr
1435
- return r;
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()}`;
1436
1499
  }
1500
+ if (x.param)
1501
+ return `:${x.ref[0].toUpperCase()}`;
1437
1502
 
1438
- function renderExpressionLiteral(x) {
1439
- // Literal value, possibly with explicit 'literal' property
1440
- switch (x.literal || typeof x.val) {
1441
- case 'number':
1442
- case 'boolean':
1443
- case 'null':
1444
- // 17.42, NULL, TRUE
1445
- return String(x.val).toUpperCase();
1446
- case 'x':
1447
- // x'f000'
1448
- return `${x.literal}'${x.val}'`;
1449
- case 'date':
1450
- case 'time':
1451
- case 'timestamp':
1452
- if (options.toSql.dialect === 'sqlite') {
1453
- // simple string literal '2017-11-02'
1454
- return `'${x.val}'`;
1455
- }
1456
- // date'2017-11-02'
1457
- return `${x.literal}'${x.val}'`;
1458
-
1459
- case 'string':
1460
- // 'foo', with proper escaping
1461
- return `'${x.val.replace(/'/g, '\'\'')}'`;
1462
- case 'object':
1463
- if (x.val === null)
1464
- return 'NULL';
1465
-
1466
- // otherwise fall through to
1467
- default:
1468
- throw new Error(`Unknown literal or type: ${JSON.stringify(x)}`);
1469
- }
1470
- }
1471
-
1472
- function renderExpressionRef(x) {
1473
- if (!x.param && !x.global) {
1474
- const magicReplacement = getVariableReplacement(x.ref, options);
1475
-
1476
- if (x.ref[0] === '$user') {
1477
- if (magicReplacement !== null)
1478
- return `'${magicReplacement}'`;
1479
-
1480
- const result = render$user(x);
1481
- // Invalid second path step doesn't cause a return
1482
- if (result)
1483
- return result;
1484
- }
1485
- else if (x.ref[0] === '$at') {
1486
- const result = render$at(x);
1487
- // Invalid second path step doesn't cause a return
1488
- if (result)
1489
- return result;
1490
- }
1491
- else if (x.ref[0] === '$session' && magicReplacement !== null) {
1492
- return `'${magicReplacement}'`;
1493
- }
1494
- }
1495
- // FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
1496
- // assume that it was not if the path has length 2 (
1497
- if (firstPathStepId(x.ref) === '$parameters' && x.ref.length === 2) {
1498
- // Parameters must be uppercased and unquoted in SQL
1499
- return `:${x.ref[1].toUpperCase()}`;
1500
- }
1501
- if (x.param)
1502
- return `:${x.ref[0].toUpperCase()}`;
1503
-
1504
- return x.ref.map(renderPathStep)
1505
- .filter(s => s !== '')
1506
- .join('.');
1507
- }
1503
+ return x.ref.map(renderPathStep)
1504
+ .filter(s => s !== '')
1505
+ .join('.');
1508
1506
 
1509
1507
  /**
1510
- * @param {object} x
1511
1508
  * @returns {string|null} Null in case of an invalid second path step
1512
1509
  */
1513
- function render$user(x) {
1510
+ function render$user() {
1514
1511
  // FIXME: this is all not enough: we might need an explicit select item alias
1515
1512
  if (x.ref[1] === 'id') {
1516
1513
  // Keep the old-style for compatibilty with magicVars.id - instead of magicVars.user.id...
@@ -1538,18 +1535,17 @@ function toSqlDdl(csn, options) {
1538
1535
  }
1539
1536
  /**
1540
1537
  * For a given reference starting with $at, render a 'current_timestamp' literal for plain.
1541
- * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1542
- *
1543
- *
1544
- * For sqlite, we render the string-format-time (strftime) function.
1545
- * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1546
- * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1547
- * --> Therefore the comparison in the temporal where clause doesn't work properly.
1548
- *
1549
- * @param {object} x
1550
- * @returns {string|null} Null in case of an invalid second path step
1551
- */
1552
- 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() {
1553
1549
  if (x.ref[1] === 'from') {
1554
1550
  switch (options.toSql.dialect) {
1555
1551
  case 'sqlite': {
@@ -1583,18 +1579,6 @@ function toSqlDdl(csn, options) {
1583
1579
  return null;
1584
1580
  }
1585
1581
 
1586
- /**
1587
- * Renders an explicit `cast()` inside an 'xpr'.
1588
- *
1589
- * @param {object} x Expression with cast
1590
- * @param {string} value Value to cast
1591
- * @returns {string} CAST statement
1592
- */
1593
- function renderExplicitTypeCast(x, value) {
1594
- const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast);
1595
- return `CAST(${value} AS ${typeRef})`;
1596
- }
1597
-
1598
1582
  /**
1599
1583
  * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1600
1584
  *
@@ -1628,7 +1612,7 @@ function toSqlDdl(csn, options) {
1628
1612
  else if (typeof s === 'object') {
1629
1613
  // Sanity check
1630
1614
  if (!s.func && !s.id)
1631
- throw new Error(`Unknown path step object: ${JSON.stringify(s)}`);
1615
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1632
1616
 
1633
1617
  // Not really a path step but an object-like function call
1634
1618
  if (s.func)
@@ -1648,10 +1632,17 @@ function toSqlDdl(csn, options) {
1648
1632
  return result;
1649
1633
  }
1650
1634
 
1651
- throw new Error(`Unknown path step: ${JSON.stringify(s)}`);
1635
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1652
1636
  }
1653
1637
  }
1654
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
+
1655
1646
  /**
1656
1647
  * Returns a copy of 'env' with increased indentation
1657
1648
  *
@@ -1663,6 +1654,54 @@ function toSqlDdl(csn, options) {
1663
1654
  }
1664
1655
  }
1665
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
+
1666
1705
  module.exports = {
1667
1706
  toSqlDdl,
1668
1707
  };