@sap/cds-compiler 2.13.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +155 -1594
  2. package/bin/cdsc.js +144 -66
  3. package/doc/CHANGELOG_ARCHIVE.md +1592 -0
  4. package/doc/CHANGELOG_BETA.md +3 -4
  5. package/doc/CHANGELOG_DEPRECATED.md +35 -1
  6. package/doc/{DeprecatedOptions.md → DeprecatedOptions_v2.md} +3 -1
  7. package/doc/Versioning.md +20 -1
  8. package/lib/api/.eslintrc.json +2 -2
  9. package/lib/api/main.js +237 -122
  10. package/lib/api/options.js +17 -88
  11. package/lib/api/validate.js +12 -16
  12. package/lib/base/keywords.js +216 -109
  13. package/lib/base/message-registry.js +152 -37
  14. package/lib/base/messages.js +145 -83
  15. package/lib/base/model.js +44 -2
  16. package/lib/base/optionProcessorHelper.js +19 -0
  17. package/lib/checks/actionsFunctions.js +7 -5
  18. package/lib/checks/annotationsOData.js +11 -32
  19. package/lib/checks/arrayOfs.js +1 -34
  20. package/lib/checks/cdsPersistence.js +1 -0
  21. package/lib/checks/elements.js +6 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/queryNoDbArtifacts.js +2 -1
  25. package/lib/checks/selectItems.js +5 -1
  26. package/lib/checks/types.js +4 -2
  27. package/lib/checks/utils.js +2 -2
  28. package/lib/checks/validator.js +4 -5
  29. package/lib/compiler/assert-consistency.js +16 -10
  30. package/lib/compiler/base.js +1 -0
  31. package/lib/compiler/builtins.js +98 -9
  32. package/lib/compiler/checks.js +22 -70
  33. package/lib/compiler/define.js +61 -13
  34. package/lib/compiler/extend.js +79 -14
  35. package/lib/compiler/finalize-parse-cdl.js +46 -29
  36. package/lib/compiler/index.js +100 -37
  37. package/lib/compiler/moduleLayers.js +7 -0
  38. package/lib/compiler/populate.js +19 -18
  39. package/lib/compiler/propagator.js +7 -4
  40. package/lib/compiler/resolve.js +297 -234
  41. package/lib/compiler/shared.js +107 -102
  42. package/lib/compiler/tweak-assocs.js +16 -11
  43. package/lib/compiler/utils.js +5 -0
  44. package/lib/edm/annotations/genericTranslation.js +93 -21
  45. package/lib/edm/csn2edm.js +230 -115
  46. package/lib/edm/edm.js +305 -226
  47. package/lib/edm/edmPreprocessor.js +509 -438
  48. package/lib/edm/edmUtils.js +31 -45
  49. package/lib/gen/Dictionary.json +98 -22
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +10 -30
  52. package/lib/gen/language.tokens +105 -114
  53. package/lib/gen/languageLexer.interp +1 -34
  54. package/lib/gen/languageLexer.js +889 -1007
  55. package/lib/gen/languageLexer.tokens +95 -106
  56. package/lib/gen/languageParser.js +20786 -22199
  57. package/lib/json/csnVersion.js +10 -11
  58. package/lib/json/from-csn.js +59 -51
  59. package/lib/json/to-csn.js +10 -10
  60. package/lib/language/antlrParser.js +2 -2
  61. package/lib/language/docCommentParser.js +62 -39
  62. package/lib/language/errorStrategy.js +52 -40
  63. package/lib/language/genericAntlrParser.js +348 -229
  64. package/lib/language/language.g4 +629 -653
  65. package/lib/language/multiLineStringParser.js +14 -42
  66. package/lib/language/textUtils.js +44 -0
  67. package/lib/main.d.ts +46 -43
  68. package/lib/main.js +108 -79
  69. package/lib/model/csnRefs.js +34 -7
  70. package/lib/model/csnUtils.js +337 -332
  71. package/lib/model/enrichCsn.js +1 -0
  72. package/lib/model/revealInternalProperties.js +30 -10
  73. package/lib/model/sortViews.js +32 -31
  74. package/lib/modelCompare/compare.js +6 -6
  75. package/lib/optionProcessor.js +73 -46
  76. package/lib/render/.eslintrc.json +1 -1
  77. package/lib/render/DuplicateChecker.js +4 -7
  78. package/lib/render/manageConstraints.js +70 -2
  79. package/lib/render/toCdl.js +1042 -882
  80. package/lib/render/toHdbcds.js +195 -245
  81. package/lib/render/toRename.js +44 -22
  82. package/lib/render/toSql.js +225 -241
  83. package/lib/render/utils/common.js +145 -15
  84. package/lib/render/utils/sql.js +20 -19
  85. package/lib/sql-identifier.js +6 -0
  86. package/lib/transform/db/.eslintrc.json +4 -3
  87. package/lib/transform/db/associations.js +2 -2
  88. package/lib/transform/db/cdsPersistence.js +5 -15
  89. package/lib/transform/db/constraints.js +4 -2
  90. package/lib/transform/db/expansion.js +22 -16
  91. package/lib/transform/db/flattening.js +109 -80
  92. package/lib/transform/db/transformExists.js +7 -7
  93. package/lib/transform/db/views.js +9 -6
  94. package/lib/transform/draft/.eslintrc.json +2 -2
  95. package/lib/transform/draft/db.js +6 -6
  96. package/lib/transform/draft/odata.js +6 -7
  97. package/lib/transform/forHanaNew.js +62 -48
  98. package/lib/transform/forOdataNew.js +49 -50
  99. package/lib/transform/localized.js +31 -20
  100. package/lib/transform/odata/toFinalBaseType.js +16 -14
  101. package/lib/transform/odata/typesExposure.js +146 -198
  102. package/lib/transform/odata/utils.js +1 -38
  103. package/lib/transform/transformUtilsNew.js +67 -84
  104. package/lib/transform/translateAssocsToJoins.js +7 -3
  105. package/lib/transform/universalCsn/.eslintrc.json +2 -2
  106. package/lib/transform/universalCsn/coreComputed.js +16 -9
  107. package/lib/transform/universalCsn/universalCsnEnricher.js +60 -10
  108. package/lib/utils/file.js +3 -3
  109. package/lib/utils/moduleResolve.js +13 -6
  110. package/lib/utils/timetrace.js +20 -21
  111. package/package.json +35 -4
  112. package/share/messages/message-explanations.json +2 -1
  113. package/share/messages/syntax-expected-integer.md +37 -0
  114. package/doc/ApiMigration.md +0 -237
  115. package/doc/CommandLineMigration.md +0 -58
  116. package/doc/ErrorMessages.md +0 -175
  117. package/doc/FioriAnnotations.md +0 -94
  118. package/doc/ODataTransformation.md +0 -273
  119. package/lib/backends.js +0 -529
  120. package/lib/fix_antlr4-8_warning.js +0 -56
  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 -296
  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
@@ -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.sqlDialect), x, env),
101
+ func: (x, env) => renderFunc(smartFuncId(prepareIdentifier(x.func), options.sqlDialect), x, options.sqlDialect, 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.
@@ -209,7 +238,7 @@ function toSqlDdl(csn, options) {
209
238
  };
210
239
 
211
240
  // Registries for artifact and element names per CSN section
212
- const definitionsDuplicateChecker = new DuplicateChecker(options.toSql.names);
241
+ const definitionsDuplicateChecker = new DuplicateChecker(options.sqlMapping);
213
242
  const deletionsDuplicateChecker = new DuplicateChecker();
214
243
  const extensionsDuplicateChecker = new DuplicateChecker();
215
244
  const removeElementsDuplicateChecker = new DuplicateChecker();
@@ -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.sqlDialect === '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.sqlDialect === '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,9 +292,9 @@ 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
- // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src === 'sql'
297
+ // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if src === 'sql'
269
298
  // (relying on the order of dictionaries above)
270
299
  // FIXME: Should consider inter-view dependencies, too
271
300
  const sql = Object.create(null);
@@ -276,10 +305,10 @@ function toSqlDdl(csn, options) {
276
305
  const { deletions, migrations: _, ...hdbKinds } = mainResultObj;
277
306
  for (const hdbKind of Object.keys(hdbKinds)) {
278
307
  for (const name in mainResultObj[hdbKind]) {
279
- if (options.toSql.src === 'sql') {
308
+ if (options.src === 'sql') {
280
309
  let sourceString = mainResultObj[hdbKind][name];
281
310
  // Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
282
- if (options.toSql.dialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN '))
311
+ if (options.sqlDialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN '))
283
312
  sourceString = sourceString.slice('COLUMN '.length);
284
313
  sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
285
314
  }
@@ -287,7 +316,7 @@ function toSqlDdl(csn, options) {
287
316
  mainResultObj[hdbKind][name] = sqlVersionLine + mainResultObj[hdbKind][name];
288
317
  }
289
318
  }
290
- if (options.toSql.src === 'sql')
319
+ if (options.src === 'sql')
291
320
  delete mainResultObj[hdbKind];
292
321
  }
293
322
 
@@ -300,7 +329,7 @@ function toSqlDdl(csn, options) {
300
329
  mainResultObj.sql = sql;
301
330
  }
302
331
 
303
- if (options.toSql.src === 'sql')
332
+ if (options.src === 'sql')
304
333
  mainResultObj.sql = sql;
305
334
 
306
335
  for (const name in deletions)
@@ -391,7 +420,7 @@ function toSqlDdl(csn, options) {
391
420
  * @returns {string} Artifact name
392
421
  */
393
422
  function renderArtifactName(artifactName) {
394
- return quoteSqlId(getResultingName(csn, options.toSql.names, artifactName));
423
+ return quoteSqlId(getResultingName(csn, options.sqlMapping, artifactName));
395
424
  }
396
425
 
397
426
  // Render an artifact migration into the appropriate dictionary of 'resultObj'.
@@ -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' ];
@@ -544,7 +573,7 @@ function toSqlDdl(csn, options) {
544
573
  const { front, back } = getSqlSnippets(options, art);
545
574
  let result = front;
546
575
  // Only HANA has row/column tables
547
- if (options.toSql.dialect === 'hana') {
576
+ if (options.sqlDialect === 'hana') {
548
577
  if (hanaTc && hanaTc.storeType) {
549
578
  // Explicitly specified
550
579
  result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
@@ -575,7 +604,7 @@ function toSqlDdl(csn, options) {
575
604
  // for `to.sql` w/ dialect `hana` the constraints will be part of the
576
605
  const constraintsAsAlter = !options.constraintsInCreateTable && options.src === 'sql' && options.sqlDialect === 'hana';
577
606
  if ( !constraintsAsAlter && art.$tableConstraints && art.$tableConstraints.referential) {
578
- const renderReferentialConstraintsAsHdbconstraint = options.toSql.src === 'hdi';
607
+ const renderReferentialConstraintsAsHdbconstraint = options.src === 'hdi';
579
608
  const referentialConstraints = {};
580
609
  Object.entries(art.$tableConstraints.referential)
581
610
  .forEach(([ fileName, referentialConstraint ]) => {
@@ -598,7 +627,7 @@ function toSqlDdl(csn, options) {
598
627
  const uniqueConstraints = art.$tableConstraints && art.$tableConstraints.unique;
599
628
  for (const cn in uniqueConstraints) {
600
629
  const c = uniqueConstraints[cn];
601
- if (options.toSql.src === 'hdi') {
630
+ if (options.src === 'hdi') {
602
631
  resultObj.hdbindex[`${artifactName}.${cn}`]
603
632
  = `UNIQUE INVERTED INDEX ${renderArtifactName(`${artifactName}_${cn}`)} ON ${tableName} (${c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
604
633
  }
@@ -608,24 +637,24 @@ function toSqlDdl(csn, options) {
608
637
  }
609
638
  result += `${env.indent}\n)`;
610
639
 
611
- if (options.toSql.dialect === 'hana')
640
+ if (options.sqlDialect === 'hana')
612
641
  result += renderTechnicalConfiguration(art.technicalConfig, childEnv);
613
642
 
614
643
 
615
644
  const associations = Object.keys(art.elements).map(name => renderAssociationElement(name, art.elements[name], childEnv))
616
645
  .filter(s => s !== '')
617
646
  .join(',\n');
618
- if (associations !== '' && options.toSql.dialect === 'hana') {
647
+ if (associations !== '' && options.sqlDialect === 'hana') {
619
648
  result += `${env.indent} WITH ASSOCIATIONS (\n${associations}\n`;
620
649
  result += `${env.indent})`;
621
650
  }
622
651
  // Only HANA has indices
623
652
  // FIXME: Really? We should provide a DB-agnostic way to specify that
624
- if (options.toSql.dialect === 'hana')
653
+ if (options.sqlDialect === '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;
@@ -729,7 +758,7 @@ function toSqlDdl(csn, options) {
729
758
  result += ` DEFAULT ${renderExpr(elm.default, env)}`;
730
759
 
731
760
  // Only HANA has fuzzy indices
732
- if (fzindex && options.toSql.dialect === 'hana')
761
+ if (fzindex && options.sqlDialect === 'hana')
733
762
  result += ` ${renderExpr(fzindex, env)}`;
734
763
 
735
764
  // (table) elements can only have a @sql.append
@@ -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
  }
@@ -877,7 +906,7 @@ function toSqlDdl(csn, options) {
877
906
  throw new ModelError(`Unexpected form of index: "${index}"`);
878
907
 
879
908
  let indexName = renderArtifactName(`${artifactName}.${index[i + 1].ref}`);
880
- if (options.toSql.names === 'plain')
909
+ if (options.sqlMapping === 'plain')
881
910
  indexName = indexName.replace(/(\.|::)/g, '_');
882
911
 
883
912
  const result = index.slice(0, i + 1); // CREATE UNIQUE INDEX
@@ -916,7 +945,7 @@ function toSqlDdl(csn, options) {
916
945
  let result = `${renderViewSource(artifactName, source.args[0], env)}`;
917
946
  for (let i = 1; i < source.args.length; i++) {
918
947
  result = `(${result} ${source.join.toUpperCase()} `;
919
- if (options.toSql.dialect === 'hana')
948
+ if (options.sqlDialect === 'hana')
920
949
  result += renderJoinCardinality(source.cardinality);
921
950
  result += `JOIN ${renderViewSource(artifactName, source.args[i], env)}`;
922
951
  if (source.on)
@@ -979,7 +1008,7 @@ function toSqlDdl(csn, options) {
979
1008
  let result = renderAbsolutePath(path, ':', env);
980
1009
 
981
1010
  // Take care of aliases
982
- const implicitAlias = path.ref.length === 0 ? getLastPartOf(getResultingName(csn, options.toSql.names, path.ref[0])) : getLastPartOfRef(path.ref);
1011
+ const implicitAlias = path.ref.length === 0 ? getLastPartOf(getResultingName(csn, options.sqlMapping, path.ref[0])) : getLastPartOfRef(path.ref);
983
1012
  if (path.as) {
984
1013
  // Source had an alias - render it
985
1014
  result += ` AS ${quoteSqlId(path.as)}`;
@@ -1089,14 +1118,15 @@ 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) {
1099
- if (isDeprecatedEnabled(options, 'renderVirtualElements'))
1128
+ if (leaf && elements[leaf] && elements[leaf].virtual) {
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)}`;
1102
1132
  }
@@ -1124,18 +1154,18 @@ 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)
1135
1165
  .map(name => renderAssociationElement(name, art.elements[name], childEnv))
1136
1166
  .filter(s => s !== '')
1137
1167
  .join(',\n');
1138
- if (associations !== '' && options.toSql.dialect === 'hana') {
1168
+ if (associations !== '' && options.sqlDialect === 'hana') {
1139
1169
  result += `${env.indent}\nWITH ASSOCIATIONS (\n${associations}\n`;
1140
1170
  result += `${env.indent})`;
1141
1171
  }
@@ -1167,7 +1197,7 @@ function toSqlDdl(csn, options) {
1167
1197
  // this would be an incompatible change, as non-uppercased, quoted identifiers
1168
1198
  // are rejected by the HANA compiler.
1169
1199
  let pIdentifier;
1170
- if (options.toSql.names === 'quoted' || options.toSql.names === 'hdbcds')
1200
+ if (options.sqlMapping === 'quoted' || options.sqlMapping === 'hdbcds')
1171
1201
  pIdentifier = prepareIdentifier(pn);
1172
1202
  else
1173
1203
  pIdentifier = quoteSqlId(pn);
@@ -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)}`;
@@ -1355,7 +1386,7 @@ function toSqlDdl(csn, options) {
1355
1386
  'cds.LocalTime': 'cds.Time',
1356
1387
  };
1357
1388
  const tName = forHanaRenamesToEarly[typeName] || typeName;
1358
- const types = cdsToSqlTypes[options.toSql.dialect];
1389
+ const types = cdsToSqlTypes[options.sqlDialect];
1359
1390
  return types && types[tName] || cdsToSqlTypes.standard[tName] || 'CHAR';
1360
1391
  }
1361
1392
 
@@ -1395,7 +1426,7 @@ function toSqlDdl(csn, options) {
1395
1426
 
1396
1427
  if (elm.srid !== undefined) {
1397
1428
  // SAP HANA Geometry types translate into CHAR in plain/sqlite (give them the default length of 2000)
1398
- if (options.toSql.dialect !== 'hana')
1429
+ if (options.sqlDialect !== 'hana')
1399
1430
  params.push(2000);
1400
1431
  else
1401
1432
  params.push(elm.srid);
@@ -1403,218 +1434,128 @@ 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);
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.sqlDialect === 'sqlite') {
1452
+ // simple string literal '2017-11-02'
1453
+ return `'${x.val}'`;
1454
+ }
1455
+ // date'2017-11-02'
1456
+ return `${x.literal}'${x.val}'`;
1457
+
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';
1464
+
1465
+ // otherwise fall through to
1466
+ default:
1467
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1429
1468
  }
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
1469
+ }
1432
1470
 
1433
- return String(expr).toUpperCase();
1471
+ function renderExpressionRef(x, env) {
1472
+ if (!x.param && !x.global) {
1473
+ const magicReplacement = getVariableReplacement(x.ref, options);
1434
1474
 
1475
+ if (x.ref[0] === '$user') {
1476
+ if (magicReplacement !== null)
1477
+ return renderStringForSql(magicReplacement, options.sqlDialect);
1435
1478
 
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 '';
1479
+ const result = render$user();
1480
+ // Invalid second path step doesn't cause a return
1481
+ if (result)
1482
+ return result;
1456
1483
  }
1457
- // Reference: Array of path steps, possibly preceded by ':'
1458
- else if (x.ref) {
1459
- return renderExpressionRef(x);
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;
1460
1489
  }
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));
1490
+ else if (x.ref[0] === '$session' && magicReplacement !== null) {
1491
+ return renderStringForSql(magicReplacement, options.sqlDialect);
1467
1492
  }
1468
- // Nested expression
1469
- else if (x.xpr) {
1470
- if (nestedExpr && !x.cast)
1471
- return `(${renderExpr(x.xpr, env, inline, true)})`;
1472
-
1473
- return renderExpr(x.xpr, env, inline, true);
1474
- }
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))})`;
1479
- }
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))}`;
1493
+ else if (x.ref[0] === '$now') { // TODO: Can there be cases where $now is followed by something?
1494
+ switch (options.sqlDialect) {
1495
+ case 'sqlite':
1496
+ case 'hana':
1497
+ return 'CURRENT_TIMESTAMP';
1498
+ case 'postgres':
1499
+ return 'current_timestamp';
1500
+ default:
1501
+ return quoteSqlId(x.ref[0]);
1502
+ }
1483
1503
  }
1484
-
1485
- throw new ModelError(`Unknown expression: ${JSON.stringify(x)}`);
1486
1504
  }
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
- }
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
- }
1505
+ // FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
1506
+ // assume that it was not if the path has length 2 (
1507
+ if (firstPathStepId(x.ref) === '$parameters' && x.ref.length === 2) {
1508
+ // Parameters must be uppercased and unquoted in SQL
1509
+ return `:${x.ref[1].toUpperCase()}`;
1527
1510
  }
1511
+ if (x.param)
1512
+ return `:${x.ref[0].toUpperCase()}`;
1528
1513
 
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
- }
1514
+ return x.ref.map(renderPathStep)
1515
+ .filter(s => s !== '')
1516
+ .join('.');
1565
1517
 
1566
1518
  /**
1567
- * @param {object} x
1568
1519
  * @returns {string|null} Null in case of an invalid second path step
1569
1520
  */
1570
- function render$user(x) {
1571
- // FIXME: this is all not enough: we might need an explicit select item alias
1521
+ function render$user() {
1522
+ // FIXME: this is all not enough: we might need an explicit select item alias (?)
1572
1523
  if (x.ref[1] === 'id') {
1573
- // Keep the old-style for compatibilty with magicVars.id - instead of magicVars.user.id...
1574
- if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String)
1575
- return `'${options.toSql.user}'`;
1576
-
1577
- else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String))
1578
- return `'${options.toSql.user.id}'`;
1579
-
1580
- if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
1581
- warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1582
- return '\'$user.id\'';
1583
- }
1584
- return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1524
+ if (options.sqlDialect === 'hana')
1525
+ return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1526
+ warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1527
+ return '\'$user.id\'';
1585
1528
  }
1586
1529
  else if (x.ref[1] === 'locale') {
1587
- if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
1588
- return (options.toSql.user && options.toSql.user.locale)
1589
- ? `'${options.toSql.user && options.toSql.user.locale}'` : '\'en\'';
1590
- }
1591
- return 'SESSION_CONTEXT(\'LOCALE\')';
1530
+ if (options.sqlDialect === 'hana')
1531
+ return 'SESSION_CONTEXT(\'LOCALE\')';
1532
+ return '\'en\''; // default language
1592
1533
  }
1593
1534
  // Basically: Second path step was invalid, do nothing - should not happen, see 'unknownMagic.js'
1594
1535
  return null;
1595
1536
  }
1596
1537
  /**
1597
1538
  * 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) {
1539
+ * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1540
+ *
1541
+ *
1542
+ * For sqlite, we render the string-format-time (strftime) function.
1543
+ * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1544
+ * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1545
+ * --> Therefore the comparison in the temporal where clause doesn't work properly.
1546
+ *
1547
+ * @returns {string|null} Null in case of an invalid second path step
1548
+ */
1549
+ function render$at() {
1610
1550
  if (x.ref[1] === 'from') {
1611
- switch (options.toSql.dialect) {
1551
+ switch (options.sqlDialect) {
1612
1552
  case 'sqlite': {
1613
1553
  const dateFromFormat = '%Y-%m-%dT%H:%M:%S.000Z';
1614
1554
  return `strftime('${dateFromFormat}', 'now')`;
1615
1555
  }
1616
1556
  case 'hana':
1617
1557
  return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1558
+ case 'postgres':
1618
1559
  case 'plain':
1619
1560
  return 'current_timestamp';
1620
1561
  default:
@@ -1623,7 +1564,7 @@ function toSqlDdl(csn, options) {
1623
1564
  }
1624
1565
 
1625
1566
  if (x.ref[1] === 'to') {
1626
- switch (options.toSql.dialect) {
1567
+ switch (options.sqlDialect) {
1627
1568
  case 'sqlite': {
1628
1569
  // + 1ms compared to $at.from
1629
1570
  const dateToFormat = '%Y-%m-%dT%H:%M:%S.001Z';
@@ -1631,6 +1572,7 @@ function toSqlDdl(csn, options) {
1631
1572
  }
1632
1573
  case 'hana':
1633
1574
  return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1575
+ case 'postgres':
1634
1576
  case 'plain':
1635
1577
  return 'current_timestamp';
1636
1578
  default:
@@ -1640,18 +1582,6 @@ function toSqlDdl(csn, options) {
1640
1582
  return null;
1641
1583
  }
1642
1584
 
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
1585
  /**
1656
1586
  * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1657
1587
  *
@@ -1664,7 +1594,6 @@ function toSqlDdl(csn, options) {
1664
1594
  if (typeof (s) === 'string') {
1665
1595
  // TODO: When is this actually executed and not handled already in renderExpr?
1666
1596
  const magicForHana = {
1667
- $now: 'CURRENT_TIMESTAMP',
1668
1597
  '$user.id': 'SESSION_CONTEXT(\'APPLICATIONUSER\')',
1669
1598
  '$user.locale': 'SESSION_CONTEXT(\'LOCALE\')',
1670
1599
  };
@@ -1672,7 +1601,7 @@ function toSqlDdl(csn, options) {
1672
1601
  if (idx === 0) {
1673
1602
  // HANA-specific translation of '$now' and '$user'
1674
1603
  // FIXME: this is all not enough: we might need an explicit select item alias
1675
- if (magicForHana[s])
1604
+ if (options.sqlDialect === 'hana' && magicForHana[s])
1676
1605
  return magicForHana[s];
1677
1606
 
1678
1607
  // Ignore initial $projection and initial $self
@@ -1709,6 +1638,13 @@ function toSqlDdl(csn, options) {
1709
1638
  }
1710
1639
  }
1711
1640
 
1641
+ function renderWindowFunction(funcName, node, fctEnv) {
1642
+ const suffix = node.xpr[0]; // OVER
1643
+ let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)})`;
1644
+ r += ` ${suffix} (${renderExpr(node.xpr.slice(1), fctEnv)})`; // do not pass suffix in renderExpr
1645
+ return r;
1646
+ }
1647
+
1712
1648
  /**
1713
1649
  * Returns a copy of 'env' with increased indentation
1714
1650
  *
@@ -1720,6 +1656,54 @@ function toSqlDdl(csn, options) {
1720
1656
  }
1721
1657
  }
1722
1658
 
1659
+ /**
1660
+ * Render the given string for SQL databases.
1661
+ *
1662
+ * @param {string} str
1663
+ * @param {string} sqlDialect
1664
+ * @return {string}
1665
+ */
1666
+ function renderStringForSql(str, sqlDialect) {
1667
+ if (sqlDialect === 'hana' || sqlDialect === 'sqlite') {
1668
+ // SQLite
1669
+ // ======
1670
+ // SQLite's tokenizer available at
1671
+ // <https://www.sqlite.org/src/file?name=src/tokenize.c>.
1672
+ //
1673
+ // Note that NUL may have side effects, as explained on
1674
+ // <https://sqlite.org/nulinstr.html>.
1675
+ //
1676
+ //
1677
+ // HANA
1678
+ // ====
1679
+ // Respects the specification available at
1680
+ // <https://help.sap.com/doc/9b40bf74f8644b898fb07dabdd2a36ad/2.0.04/en-US/SAP_HANA_SQL_Reference_Guide_en.pdf>.
1681
+ //
1682
+ // <string_literal> ::= <single_quote>[<any_character>...]<single_quote>
1683
+ // <single_quote> ::= '
1684
+ //
1685
+ // and
1686
+ // > # Quotation marks
1687
+ // > Single quotation marks are used to delimit string literals.
1688
+ // > A single quotation mark itself can be represented using two single quotation marks.
1689
+ str = str.replace(/'/g, '\'\'')
1690
+ .replace(/\u{0}/ug, '\' || CHAR(0) || \'');
1691
+ }
1692
+ else {
1693
+ // Generic SQL databases
1694
+ // =====================
1695
+ // While escaping NUL may be useful to avoid the SQL file being identified as binary,
1696
+ // we can't escape it using `CHAR(0)`. This function is not available on e.g. PostgreSQL.
1697
+ // On top of this, PostgreSQL also has this limitation:
1698
+ // > chr(int) | text | Character with the given code. For UTF8 the argument is treated as a Unicode code point.
1699
+ // > | | For other multibyte encodings the argument must designate an ASCII character. The NULL (0)
1700
+ // > | | character is not allowed because text data types cannot store such bytes.
1701
+ // - <https://www.postgresql.org/docs/9.1/functions-string.html>
1702
+ str = str.replace(/'/g, '\'\'');
1703
+ }
1704
+ return `'${str}'`;
1705
+ }
1706
+
1723
1707
  module.exports = {
1724
1708
  toSqlDdl,
1725
1709
  };