Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/api/CatalogApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,21 @@ const query = {
getAvailableExtensions: function () {
return `SELECT name, installed_version FROM pg_available_extensions`;
},
/**
*
* @param {String[]} schemas
*/
getEnumTypes: function (schemas) {
return `SELECT n.nspname AS schema_name, t.typname AS type_name, e.enumlabel AS enum_value
FROM pg_type t
JOIN pg_enum e ON e.enumtypid = t.oid
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname IN ('${schemas.join("','")}')
AND t.oid NOT IN (
SELECT d.objid FROM pg_depend d WHERE d.deptype = 'e'
)
ORDER BY n.nspname, t.typname, e.enumsortorder`;
},
};

class CatalogApi {
Expand Down Expand Up @@ -827,6 +842,25 @@ class CatalogApi {
return result;
}

/**
*
* @param {import("pg").Client} client
* @param {import("../models/config")} config
*/
static async retrieveEnumTypes(client, config) {
let result = {};

const rows = await client.query(query.getEnumTypes(config.compareOptions.schemaCompare.namespaces));

rows.rows.forEach((row) => {
const fullTypeName = `"${row.schema_name}"."${row.type_name}"`;
if (!result[fullTypeName]) result[fullTypeName] = { values: [] };
result[fullTypeName].values.push(row.enum_value);
});

return result;
}

/**
*
* @param {import("pg").Client} client
Expand Down
73 changes: 62 additions & 11 deletions src/api/CompareApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class CompareApi {
dbObjects.aggregates = await catalogApi.retrieveAggregates(client, config);
dbObjects.sequences = await catalogApi.retrieveSequences(client, config);
dbObjects.extensions = await catalogApi.retrieveExtensions(client);
dbObjects.enumTypes = await catalogApi.retrieveEnumTypes(client, config);

//TODO: Add a way to retrieve AGGREGATE and WINDOW functions
//TODO: Do we need to retrieve roles?
Expand Down Expand Up @@ -142,6 +143,10 @@ class CompareApi {
) {
let sqlPatch = [];

let enumScripts = this.compareEnumTypes(dbSourceObjects.enumTypes, dbTargetObjects.enumTypes);
sqlPatch.push(...enumScripts.topScripts);
eventEmitter.emit("compare", "ENUM TYPE objects have been compared", 43);

sqlPatch.push(...this.compareExtensions(dbSourceObjects.extensions, dbTargetObjects.extensions));
eventEmitter.emit("compare", "SCHEMA objects have been compared", 45);

Expand Down Expand Up @@ -188,9 +193,50 @@ class CompareApi {
sqlPatch.push(...this.compareTablesTriggers(dbSourceObjects.tables, dbTargetObjects.tables, addedTables));
eventEmitter.emit("compare", "TRIGGER objects have been compared", 85);

sqlPatch.push(...enumScripts.bottomScripts);

return sqlPatch;
}

/**
*
* @param {Object} sourceEnumTypes
* @param {Object} targetEnumTypes
* @returns {{ topScripts: String[], bottomScripts: String[] }}
*/
static compareEnumTypes(sourceEnumTypes, targetEnumTypes) {
let topScripts = [];
let bottomScripts = [];

for (let sourceType in sourceEnumTypes) {
if (!targetEnumTypes[sourceType]) {
topScripts.push(...this.finalizeScript(`CREATE ENUM TYPE ${sourceType}`, [sql.generateCreateEnumTypeScript(sourceType, sourceEnumTypes[sourceType].values)]));
} else {
const tgtSet = new Set(targetEnumTypes[sourceType].values);
const srcSet = new Set(sourceEnumTypes[sourceType].values);
const newValues = sourceEnumTypes[sourceType].values.filter((v) => !tgtSet.has(v));
const removedValues = targetEnumTypes[sourceType].values.filter((v) => !srcSet.has(v));

if (newValues.length > 0)
topScripts.push(...this.finalizeScript(`ALTER ENUM TYPE ${sourceType}`, [sql.generateAddEnumValueScript(sourceType, newValues)]));

if (removedValues.length > 0)
bottomScripts.push(
...this.finalizeScript(`WARNING ENUM TYPE ${sourceType}`, [
sql.generateRebuildEnumTypeWarningScript(sourceType, sourceEnumTypes[sourceType].values, targetEnumTypes[sourceType].values),
])
);
}
}

for (let targetType in targetEnumTypes) {
if (!sourceEnumTypes[targetType])
bottomScripts.push(...this.finalizeScript(`DROP ENUM TYPE ${targetType}`, [sql.generateDropEnumTypeScript(targetType)]));
}

return { topScripts, bottomScripts };
}

/**
*
* @param {String} scriptLabel
Expand Down Expand Up @@ -225,7 +271,7 @@ class CompareApi {
sqlScript.push(sql.generateChangeCommentScript(objectType.SCHEMA, sourceSchema, sourceSchemas[sourceSchema].comment));
}

if (targetSchemas[sourceSchema] && sourceSchemas[sourceSchema].comment != targetSchemas[sourceSchema].comment)
if (targetSchemas[sourceSchema] && CompareApi.normalizeComment(sourceSchemas[sourceSchema].comment) != CompareApi.normalizeComment(targetSchemas[sourceSchema].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.SCHEMA, sourceSchema, sourceSchemas[sourceSchema].comment));

finalizedScript.push(...this.finalizeScript(`CREATE OR UPDATE SCHEMA ${sourceSchema}`, sqlScript));
Expand Down Expand Up @@ -299,7 +345,7 @@ class CompareApi {
if (sourceTables[sourceTable].owner != dbTargetObjects.tables[sourceTable].owner)
sqlScript.push(sql.generateChangeTableOwnerScript(sourceTable, sourceTables[sourceTable].owner));

if (sourceTables[sourceTable].comment != dbTargetObjects.tables[sourceTable].comment)
if (CompareApi.normalizeComment(sourceTables[sourceTable].comment) != CompareApi.normalizeComment(dbTargetObjects.tables[sourceTable].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.TABLE, sourceTable, sourceTables[sourceTable].comment));
} else {
//Table not exists on target database, then generate the script to create table
Expand Down Expand Up @@ -502,7 +548,7 @@ class CompareApi {
sqlScript.push(sql.generateChangeTableColumnScript(tableName, columnName, changes));
}

if (sourceTableColumn.comment != targetTableColumn.comment)
if (CompareApi.normalizeComment(sourceTableColumn.comment) != CompareApi.normalizeComment(targetTableColumn.comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.COLUMN, `${tableName}.${columnName}`, sourceTableColumn.comment));

return sqlScript;
Expand Down Expand Up @@ -538,7 +584,7 @@ class CompareApi {
sql.generateChangeCommentScript(objectType.CONSTRAINT, constraint, sourceTableConstraints[constraint].comment, tableName)
);
} else {
if (sourceTableConstraints[constraint].comment != targetTableConstraints[constraint].comment)
if (CompareApi.normalizeComment(sourceTableConstraints[constraint].comment) != CompareApi.normalizeComment(targetTableConstraints[constraint].comment))
sqlScript.push(
sql.generateChangeCommentScript(
objectType.CONSTRAINT,
Expand Down Expand Up @@ -605,7 +651,7 @@ class CompareApi {
)
);
} else {
if (sourceTableIndexes[index].comment != targetTableIndexes[index].comment)
if (CompareApi.normalizeComment(sourceTableIndexes[index].comment) != CompareApi.normalizeComment(targetTableIndexes[index].comment))
sqlScript.push(
sql.generateChangeCommentScript(
objectType.INDEX,
Expand Down Expand Up @@ -726,7 +772,7 @@ class CompareApi {
if (sourceTableTriggers[trigger].definition != targetTableTriggers[trigger].definition) {
sqlScript.push(sql.generateDropTriggerScript(tableName, trigger));
sqlScript.push(sql.generateCreateTriggerScript(sourceTableTriggers[trigger]));
if (sourceTableTriggers[trigger].comment != targetTableTriggers[trigger].comment)
if (CompareApi.normalizeComment(sourceTableTriggers[trigger].comment) != CompareApi.normalizeComment(targetTableTriggers[trigger].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.TRIGGER, trigger, sourceTableTriggers[trigger].comment, tableName));
}
} else {
Expand Down Expand Up @@ -778,7 +824,7 @@ class CompareApi {
if (sourceViews[view].owner != targetViews[view].owner)
sqlScript.push(sql.generateChangeTableOwnerScript(view, sourceViews[view].owner));

if (sourceViews[view].comment != targetViews[view].comment)
if (CompareApi.normalizeComment(sourceViews[view].comment) != CompareApi.normalizeComment(targetViews[view].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.VIEW, view, sourceViews[view].comment));
}
} else {
Expand Down Expand Up @@ -852,7 +898,7 @@ class CompareApi {
if (sourceMaterializedViews[view].owner != targetMaterializedViews[view].owner)
sqlScript.push(sql.generateChangeTableOwnerScript(view, sourceMaterializedViews[view].owner));

if (sourceMaterializedViews[view].comment != targetMaterializedViews[view].comment)
if (CompareApi.normalizeComment(sourceMaterializedViews[view].comment) != CompareApi.normalizeComment(targetMaterializedViews[view].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.MATERIALIZED_VIEW, view, sourceMaterializedViews[view].comment));
}
} else {
Expand Down Expand Up @@ -932,7 +978,7 @@ class CompareApi {
)
);

if (sourceFunctions[procedure][procedureArgs].comment != sourceFunctions[procedure][procedureArgs].comment)
if (CompareApi.normalizeComment(sourceFunctions[procedure][procedureArgs].comment) != CompareApi.normalizeComment(targetFunctions[procedure][procedureArgs].comment))
sqlScript.push(
sql.generateChangeCommentScript(
procedureType,
Expand Down Expand Up @@ -1018,7 +1064,7 @@ class CompareApi {
sql.generateChangeAggregateOwnerScript(aggregate, aggregateArgs, sourceAggregates[aggregate][aggregateArgs].owner)
);

if (sourceAggregates[aggregate][aggregateArgs].comment != targetAggregates[aggregate][aggregateArgs].comment)
if (CompareApi.normalizeComment(sourceAggregates[aggregate][aggregateArgs].comment) != CompareApi.normalizeComment(targetAggregates[aggregate][aggregateArgs].comment))
sqlScript.push(
sql.generateChangeCommentScript(
objectType.AGGREGATE,
Expand Down Expand Up @@ -1117,7 +1163,7 @@ class CompareApi {
...this.compareSequencePrivileges(sequence, sourceSequences[sequence].privileges, targetSequences[targetSequence].privileges)
);

if (sourceSequences[sequence].comment != targetSequences[targetSequence].comment)
if (CompareApi.normalizeComment(sourceSequences[sequence].comment) != CompareApi.normalizeComment(targetSequences[targetSequence].comment))
sqlScript.push(sql.generateChangeCommentScript(objectType.SEQUENCE, sequence, sourceSequences[sequence].comment));
} else {
//Sequence not exists on target database, then generate the script to create sequence
Expand Down Expand Up @@ -1650,6 +1696,11 @@ class CompareApi {

return finalizedScript;
}

static normalizeComment(comment) {
if (!comment) return comment;
return comment.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
}

module.exports = CompareApi;
1 change: 1 addition & 0 deletions src/enums/objectType.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ module.exports = {
SEQUENCE: "SEQUENCE",
PROCEDURE: "PROCEDURE",
TRIGGER: "TRIGGER",
ENUM_TYPE: "ENUM TYPE",
};
2 changes: 2 additions & 0 deletions src/models/databaseObjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class DatabaseObjects {
this.sequences = null;
/** @type {Object} The definition of extensions*/
this.extensions = null;
/** @type {Object} The definition of enum types*/
this.enumTypes = null;
}
}

Expand Down
46 changes: 46 additions & 0 deletions src/sqlScriptGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,52 @@ CREATE SEQUENCE IF NOT EXISTS ${sequence}
let script = `\nALTER EXTENSION "${name}" UPDATE TO '${version}';${hints.extensionToUpdate}\n`;
return script;
},
/**
*
* @param {String} typeName fully-qualified enum type name (e.g. "public"."my_type")
* @param {String[]} values ordered list of enum values
*/
generateCreateEnumTypeScript: function (typeName, values) {
const valueList = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
return `\nCREATE TYPE ${typeName} AS ENUM (${valueList});\n`;
},
/**
*
* @param {String} typeName fully-qualified enum type name
* @param {String[]} newValues values to add (must not already exist in target)
*/
generateAddEnumValueScript: function (typeName, newValues) {
const lines = newValues.map((v) => `ALTER TYPE ${typeName} ADD VALUE IF NOT EXISTS '${v.replace(/'/g, "''")}';`);
return `\n${lines.join("\n")}\n`;
},
/**
*
* @param {String} typeName fully-qualified enum type name
*/
generateDropEnumTypeScript: function (typeName) {
return `\nDROP TYPE IF EXISTS ${typeName};\n`;
},
/**
* Generates a warning comment block when enum values were removed in source.
* PostgreSQL does not support removing enum values without recreating the type.
*
* @param {String} typeName fully-qualified enum type name
* @param {String[]} sourceValues desired values (from source)
* @param {String[]} targetValues current values (in target)
*/
generateRebuildEnumTypeWarningScript: function (typeName, sourceValues, targetValues) {
const srcSet = new Set(sourceValues);
const removedValues = targetValues.filter((v) => !srcSet.has(v));
const valueList = sourceValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
return [
`--WARN: Enum values present in TARGET but not in SOURCE: [${removedValues.join(", ")}]`,
`--WARN: PostgreSQL does not support removing enum values without recreating the type. Manual action required:`,
`-- 1. ALTER TYPE ${typeName} RENAME TO <temp_name>;`,
`-- 2. CREATE TYPE ${typeName} AS ENUM (${valueList});`,
`-- 3. Migrate all columns and objects that reference this type.`,
`-- 4. DROP TYPE <temp_name>;`,
].join("\n");
},
};

module.exports = helper;