@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
218 lines (189 loc) • 8.45 kB
JavaScript
// Render functions for toSql.js
;
const { getResultingName } = require('../../model/csnUtils');
const { smartId, delimitedId } = require('../../sql-identifier');
const { ModelError } = require('../../base/error');
const { isBetaEnabled, setProp } = require('../../base/model');
/**
* Render a given referential constraint as part of a SQL CREATE TABLE statement, or as .hdbconstraint artefact.
*
* @param {CSN.ReferentialConstraint} constraint Content of the constraint
* @param {string} indent Indent to render the SQL with
* @param {boolean} toUpperCase Whether to uppercase the identifier
* @param {CSN.Model} csn CSN
* @param {CSN.Options} options is needed for the naming mode and the sql dialect
* @param {boolean} [alterConstraint=false] whether the constraint should be rendered as part of an ALTER TABLE statement
*
* @returns {string} SQL statement which can be used to create the referential constraint on the db.
*/
function renderReferentialConstraint( constraint, indent, toUpperCase, csn, options, alterConstraint = false ) {
const quoteId = getIdentifierUtils(csn, options).quoteSqlId;
if (toUpperCase) {
constraint.identifier = constraint.identifier.toUpperCase();
constraint.foreignKey = constraint.foreignKey.map(fk => fk.toUpperCase());
constraint.parentKey = constraint.parentKey.map(fk => fk.toUpperCase());
constraint.dependentTable = constraint.dependentTable.toUpperCase();
constraint.parentTable = constraint.parentTable.toUpperCase();
}
const renderAsHdbconstraint = options.transformation === 'hdbcds' ||
options.src === 'hdi';
const { sqlMapping, sqlDialect } = options;
let result = '';
result += `${ indent }CONSTRAINT ${ quoteId(constraint.identifier) }\n`;
if (renderAsHdbconstraint)
result += `${ indent }ON ${ quoteId(getResultingName(csn, sqlMapping, constraint.dependentTable)) }\n`;
if (!alterConstraint) {
result += `${ indent }FOREIGN KEY(${ constraint.foreignKey.map(quoteId).join(', ') })\n`;
result += `${ indent }REFERENCES ${ quoteId(getResultingName(csn, sqlMapping, constraint.parentTable)) }(${ constraint.parentKey.map(quoteId).join(', ') })\n`;
const onDeleteRemark = constraint.onDeleteRemark ? ` -- ${ constraint.onDeleteRemark }` : '';
// omit 'RESTRICT' action for ON UPDATE / ON DELETE, because it interferes with deferred constraint check
if (sqlDialect === 'sqlite' || sqlDialect === 'postgres') {
if (constraint.onDelete === 'CASCADE' )
result += `${ indent }ON DELETE ${ constraint.onDelete }${ onDeleteRemark }\n`;
}
else {
result += `${ indent }ON UPDATE RESTRICT\n`;
result += `${ indent }ON DELETE ${ constraint.onDelete }${ onDeleteRemark }\n`;
}
}
// constraint enforcement / validation must be switched off using sqlite pragma statement
// constraint enforcement / validation not supported by postgres
if (options.transformation === 'hdbcds' || (options.toSql && sqlDialect !== 'sqlite' && sqlDialect !== 'postgres')) {
result += `${ indent }${ !constraint.validated ? 'NOT ' : '' }VALIDATED\n`;
result += `${ indent }${ !constraint.enforced ? 'NOT ' : '' }ENFORCED\n`;
}
// for sqlite and postgreSQL, the DEFERRABLE keyword is required
result += `${ indent }${ sqlDialect === 'sqlite' || sqlDialect === 'postgres' ? 'DEFERRABLE ' : '' }INITIALLY DEFERRED`;
return result;
}
/**
* Get functions which can be used to prepare and quote SQL identifiers based on the options provided.
*
* @param {CSN.Options} options
* @returns quoteSqlId and prepareIdentifier function
*/
function getIdentifierUtils( csn, options ) {
return { quoteSqlId, prepareIdentifier, renderArtifactName };
/**
* Return 'name' with appropriate "-quotes.
* Additionally perform the following conversions on 'name'
* If 'options.sqlMapping' is 'plain'
* - replace '.' or '::' by '_'
* else if 'options.sqlMapping' is 'quoted'
* - replace '::' by '.'
* Complain about names that collide with known SQL keywords or functions
*
* @param {string} name Identifier to quote
* @returns {string} Quoted identifier
*/
function quoteSqlId( name ) {
name = prepareIdentifier(name);
switch (options.sqlMapping) {
case 'plain':
return smartId(name, options.sqlDialect);
case 'quoted':
return delimitedId(name, options.sqlDialect);
case 'hdbcds':
return delimitedId(name, options.sqlDialect);
default:
return undefined;
}
}
/**
* Prepare an identifier:
* If 'options.sqlMapping' is 'plain'
* - replace '.' or '::' by '_'
* else if 'options.sqlMapping' is 'quoted'
* - replace '::' by '.'
*
* @param {string} name Identifier to prepare
* @returns {string} Identifier prepared for quoting
*/
function prepareIdentifier( name ) {
// Sanity check
if (options.sqlDialect === 'sqlite' && options.sqlMapping !== 'plain')
throw new ModelError(`Not expecting ${ options.sqlMapping } names for 'sqlite' dialect`);
switch (options.sqlMapping) {
case 'plain':
return name.replace(/(\.|::)/g, '_');
case 'quoted':
return name.replace(/::/g, '.');
case 'hdbcds':
return name;
default:
throw new ModelError(`No matching rendering found for naming mode ${ options.sqlMapping }`);
}
}
/**
* Given the following artifact name: namespace.prefix.entity.with.dot, render the following,
* depending on the naming mode:
* - plain: NAMESPACE_PREFIX_ENTITY_WITH_DOT
* - quoted: namespace.prefix.entity_with_dot
* - hdbcds: namespace::prefix.entity_with_dot
*
*
* @param {string} artifactName Artifact name to render
*
* @returns {string} Artifact name
*/
function renderArtifactName( artifactName ) {
return quoteSqlId(getResultingName(csn, options.sqlMapping, artifactName));
}
}
const allowedHdbProjectionViewProperties = {
from: true,
mixin: true,
columns: true,
};
/**
* Determines if the given artifact is a projection view. Caches the information via
* non-enumerable property `$isProjectionView` on the artifact for non-trivial cases.
*
* Requires a for.hana processed model including A2J.
*
* @param {CSN.Model} csn - The Core Schema Notation (CSN) model.
* @param {CSN.Artifact} artifact - The artifact to check for being a projection view.
* @param {Model.Options} options - Compilation options, including feature flags and SQL dialect.
* @returns {boolean} `true` if the artifact is a projection view, otherwise `false`.
*/
function isProjectionView( csn, artifact, options ) {
if (!isBetaEnabled(options, 'projectionViews') || !artifact.projection || options.sqlDialect !== 'hana' || artifact.params || !artifact.$dataProductService)
return false;
if (artifact.$isProjectionView !== undefined)
return artifact.$isProjectionView;
setProp(artifact, '$isProjectionView', _artifactIsProjectionView(csn, artifact));
return artifact.$isProjectionView;
}
function _artifactIsProjectionView( csn, artifact ) {
for (const prop of Object.keys(artifact.projection)) {
if (!prop.startsWith('@') && !allowedHdbProjectionViewProperties[prop])
return false;
}
const source = csn.definitions[artifact.projection.from.ref[0]];
if (!source)
return false;
// Source must be a table or a projection view. Only support table for now
if (source.kind !== 'entity' || source.query || source.projection)
return false;
const referencedElements = Object.create(null);
for (const column of artifact.projection.columns) {
if (!column.ref)
return false;
if (referencedElements[column.ref.at(-1)])
return false; // we have an element projected more than once, seems to not be supported
referencedElements[column.ref.at(-1)] = true;
}
// Full primary key/not null chain needs to be projected according to HANA SQL spec
// https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/create-projection-view-statement-data-definition
for (const elementName in source.elements) {
const element = source.elements[elementName];
if ((element.notNull || element.key) && !referencedElements[elementName])
return false;
}
return true;
}
module.exports = {
renderReferentialConstraint,
getIdentifierUtils,
isProjectionView,
};