@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
292 lines (251 loc) • 10.1 kB
JavaScript
;
/**
* Encapsulate all the functions needed to render SQL ALTER/DROP/ADD statements.
*/
class DeltaRenderer {
constructor(options, scopedFunctions) {
this.options = options;
this.scopedFunctions = scopedFunctions;
}
/**
* Render column additions as SQL. Checks for duplicate elements.
*/
addColumnsFromElementStrings(artifactName, eltStrings) {
return eltStrings.map(eltString => `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ADD ${ eltString };`);
}
/**
* Render column additions as SQL. Checks for duplicate elements.
*/
addColumnsFromElementsObj(artifactName, elementsObj, env, duplicateChecker) {
// Only extend with 'ADD' for elements/associations
// TODO: May also include 'RENAME' at a later stage
const alterEnv = this.scopedFunctions.activateAlterMode(env);
const elements = Object.entries(elementsObj)
.map(([ name, elt ]) => this.scopedFunctions.renderElement(name, elt, duplicateChecker, null, alterEnv))
.filter(s => s !== '');
if (elements.length)
return this.addColumnsFromElementStrings(artifactName, elements);
return [];
}
/**
* By default, we don't support rendering association-alters - only for HANA
*/
addAssociations(_artifactName, _extElements, _env) {
return [];
}
/**
* Render key addition as SQL.
*/
addKey(artifactName, elementsObj) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ADD ${ this.primaryKey(elementsObj) };` ];
}
/**
* Render column removals as SQL.
*/
dropColumns(artifactName, sqlIds) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } DROP ${ sqlIds.join(', ') };` ];
}
/**
* No associations by default - only for HANA.
*/
dropAssociation(_artifactName, _sqlId) {
return [];
}
/**
* Render primary-key removals as SQL.
*/
dropKey(artifactName) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } DROP PRIMARY KEY;` ];
}
/**
* Render column modifications as SQL.
*/
alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
if (Array.isArray(definitionsStr)) {
const prefix = `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER (`;
const padding = ' '.repeat(prefix.length);
const body = definitionsStr.map(s => padding + s).join(',\n').slice(padding.length); // no padding for first part
const postfix = ');';
return [ prefix + body + postfix ];
}
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER (${ definitionsStr });` ];
}
/**
* Render primary keys as SQL.
*/
primaryKey(elementsObj) {
const primaryKeys = Object.keys(elementsObj)
.filter(name => elementsObj[name].key && !elementsObj[name].virtual)
.map(name => this.scopedFunctions.quoteSqlId(name))
.join(', ');
return primaryKeys && `PRIMARY KEY(${ primaryKeys })`;
}
/**
* Render entity-comment modifications as SQL.
*/
alterEntityComment(artifactName, comment) {
return [ `COMMENT ON TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } IS ${ this.comment(comment) };` ];
}
/**
* Render column-comment modifications as SQL.
*/
alterColumnComment(artifactName, columnName, comment) {
return [ `COMMENT ON COLUMN ${ this.scopedFunctions.renderArtifactName(artifactName) }.${ columnName } IS ${ this.comment(comment) };` ];
}
/**
* Render comment string.
*/
comment(comment) {
return comment && this.scopedFunctions.renderStringForSql(this.scopedFunctions.getHanaComment({ doc: comment }), this.options.sqlDialect) || 'NULL';
}
/**
* Alter SQL snippet for entity.
*/
alterEntitySqlSnippet(artifactName, snippet) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ${ snippet };` ];
}
/**
* Concatenate multiple statements which are to be treated as one by the API caller.
*/
concat(...statements) {
return [ statements.join('\n') ];
}
}
class DeltaRendererHana extends DeltaRenderer {
#alters = [];
#details = [];
getConsolidatedAlterColumn(artifactName) {
if (this.#alters.length === 0)
return null;
const result = [ ...this.#details, ...super.alterColumns(artifactName, null, null, this.#alters) ];
this.#alters = [];
this.#details = [];
return result;
}
/**
* Render column modifications as SQL.
*/
alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
if (delta.details)
this.#details.push(`-- [WARNING] this statement could ${ delta.lossy ? 'be lossy' : 'fail' }: ${ delta.details }`);
this.#alters.push(definitionsStr);
return [];
}
/**
* Render column additions as HANA SQL. Checks for duplicate elements.
*/
addColumnsFromElementStrings(artifactName, eltStrings) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ADD (${ eltStrings.join(', ') });` ];
}
/**
* Render association additions as HANA SQL.
* TODO duplicity check
*/
addAssociations(artifactName, elementsObj, env) {
return Object.entries(elementsObj)
.map(([ name, elt ]) => this.scopedFunctions.renderAssociationElement(name, elt, env))
.filter(s => s !== '')
.map(eltStr => `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ADD ASSOCIATION (${ eltStr });`);
}
/**
* Render column removals as HANA SQL.
*/
dropColumns(artifactName, sqlIds) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } DROP (${ sqlIds.join(', ') });` ];
}
/**
* Render association removals as HANA SQL.
*/
dropAssociation(artifactName, sqlId) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } DROP ASSOCIATION ${ sqlId };` ];
}
}
class DeltaRendererPostgres extends DeltaRenderer {
/**
* Render primary-key removals as SQL.
* @todo tableName is escaped - we cannot simply add _pkey
*/
dropKey(artifactName) {
const table = this.scopedFunctions.renderArtifactName(artifactName);
const pkey = this.scopedFunctions.renderArtifactName(`${ artifactName }_pkey`);
return [ `ALTER TABLE ${ table } DROP CONSTRAINT ${ pkey };` ];
}
/**
* Render column removals as SQL.
*/
dropColumns(artifactName, sqlIds) {
return sqlIds.map(sqlId => `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } DROP ${ sqlId };`);
}
/**
* Render column modifications as Postgres SQL - no ( ), special NOT NULL.
*/
alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
const sqls = [];
definitionsStr = this.#removeNullabilityFromElementString(delta, definitionsStr);
if (delta.old.default && !delta.old.value) // Drop old default if any exists
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER COLUMN ${ columnName } DROP DEFAULT;`);
if (delta.new.default && !delta.new.value ) { // Alter column with default
const df = delta.new.default;
delete delta.new.default;
const eltStrNoDefault = this.#removeNullabilityFromElementString(delta, this.scopedFunctions.renderElement(eltName, delta.new, null, null, env));
delta.new.default = df;
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER ${ eltStrNoDefault };`);
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER COLUMN ${ columnName } SET DEFAULT ${ this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default')) };`);
}
else { // Alter column without default
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER ${ definitionsStr };`);
}
if (delta.new.notNull && !delta.old.notNull)
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER ${ columnName } SET NOT NULL;`);
else if (delta.old.notNull && !delta.new.notNull)
sqls.push(`ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER ${ columnName } DROP NOT NULL;`);
return sqls;
}
/**
* Postgres does not support changing a column AND doing [NOT] NULL things in one statement.
* So we filter it from the SQL String and render the appropriate SET/DROP for the NOT NULL separately.
*
* @param {object} delta
* @param {string} string
* @returns {string}
*/
#removeNullabilityFromElementString(delta, string) {
if (delta.new.notNull === true || delta.new.key === true)
string = string.replace(' NOT NULL', ''); // TODO: Is this robust enough?
else if (delta.new.notNull === false || delta.new.$notNull === false)
string = string.replace(' NULL', ''); // TODO: Is this robust enough?
else if (delta.new.notNull === delta.old.notNull)
string = string.replace( delta.new.notNull ? ' NOT NULL' : ' NULL', ''); // TODO: Is this robust enough?
return string;
}
}
class DeltaRendererH2 extends DeltaRenderer {
/**
* Render column modifications as H2 SQL - no ().
*/
alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
return [ `ALTER TABLE ${ this.scopedFunctions.renderArtifactName(artifactName) } ALTER ${ definitionsStr };` ];
}
}
/**
* Return an object encapsulating the render-functions for ALTER/DROP for a given db dialect.
*
* @param {CSN.Options} options
* @param {object} scopedFunctions
* @returns {DeltaRenderer}
*/
function getDeltaRenderer( options, scopedFunctions ) {
switch (options.sqlDialect) {
case 'hana':
return new DeltaRendererHana(options, scopedFunctions);
case 'h2':
return new DeltaRendererH2(options, scopedFunctions);
case 'postgres':
return new DeltaRendererPostgres(options, scopedFunctions);
default:
return new DeltaRenderer(options, scopedFunctions);
}
}
module.exports = {
getDeltaRenderer,
};