UNPKG

pg-diff-api

Version:

PostgreSQL migration strategy for NodeJS

858 lines (791 loc) 33.6 kB
const core = require("../core"); const query = { /** * * @param {String[]} schemas */ getAllSchemas: function () { return `SELECT nspname FROM pg_namespace WHERE nspname NOT IN ('pg_catalog','information_schema') AND nspname NOT LIKE 'pg_toast%' AND nspname NOT LIKE 'pg_temp%'`; }, /** * * @param {String[]} schemas */ getSchemas: function (schemas) { //TODO: Instead of using ::regrole casting, for better performance join with pg_roles return `SELECT n.nspname, n.nspowner::regrole::name as owner, d.description as comment FROM pg_namespace n LEFT JOIN pg_description d ON d.objoid = n."oid" AND d.objsubid = 0 WHERE nspname IN ('${schemas.join("','")}')`; }, /** * * @param {String[]} schemas */ getTables: function (schemas) { return `SELECT t.schemaname, t.tablename, t.tableowner, d.description as comment FROM pg_tables t INNER JOIN pg_namespace n ON t.schemaname = n.nspname INNER JOIN pg_class c ON t.tablename = c.relname AND c.relnamespace = n."oid" LEFT JOIN pg_description d ON d.objoid = c."oid" AND d.objsubid = 0 WHERE t.schemaname IN ('${schemas.join("','")}') AND c.oid NOT IN ( SELECT d.objid FROM pg_depend d WHERE d.deptype = 'e' )`; }, /** * * @param {String} tableName */ getTableOptions: function (schemaName, tableName) { return `SELECT relhasoids FROM pg_class c INNER JOIN pg_namespace n ON n."oid" = c.relnamespace AND n.nspname = '${schemaName}' WHERE c.relname = '${tableName}'`; }, /** * * @param {String} tableName * @param {import("../models/serverVersion")} serverVersion */ getTableColumns: function (schemaName, tableName, serverVersion) { return `SELECT a.attname, a.attnotnull, t.typname, t.oid as typeid, t.typcategory, pg_get_expr(ad.adbin ,ad.adrelid ) as adsrc, ${core.checkServerCompatibility(serverVersion, 10, 0) ? "a.attidentity" : "NULL as attidentity"}, ${core.checkServerCompatibility(serverVersion, 12, 0) ? "a.attgenerated" : "NULL as attgenerated"}, CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN (a.atttypmod-4) >> 16 WHEN (t.typname = 'bpchar' or t.typname = 'varchar') AND a.atttypmod > 0 THEN a.atttypmod-4 ELSE null END AS precision, CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN (a.atttypmod-4) & 65535 ELSE null END AS scale, d.description AS comment FROM pg_attribute a INNER JOIN pg_type t ON t.oid = a.atttypid LEFT JOIN pg_attrdef ad on ad.adrelid = a.attrelid AND a.attnum = ad.adnum INNER JOIN pg_namespace n ON n.nspname = '${schemaName}' INNER JOIN pg_class c ON c.relname = '${tableName}' AND c.relnamespace = n."oid" LEFT JOIN pg_description d ON d.objoid = c."oid" AND d.objsubid = a.attnum WHERE attrelid = c."oid" AND attnum > 0 AND attisdropped = false ORDER BY a.attnum ASC`; }, /** * * @param {String} tableName */ getTableConstraints: function (schemaName, tableName) { return `SELECT c.conname, c.contype, f_sch.nspname AS foreign_schema, f_tbl.relname AS foreign_table, pg_get_constraintdef(c.oid) as definition, d.description AS comment FROM pg_constraint c INNER JOIN pg_namespace n ON n.nspname = '${schemaName}' INNER JOIN pg_class cl ON cl.relname ='${tableName}' AND cl.relnamespace = n.oid LEFT JOIN pg_class f_tbl ON f_tbl.oid = c.confrelid LEFT JOIN pg_namespace f_sch ON f_sch.oid = f_tbl.relnamespace LEFT JOIN pg_description d ON d.objoid = c."oid" AND d.objsubid = 0 WHERE c.conrelid = cl.oid`; }, /** * * @param {String} schemaName * @param {String} tableName */ getTableIndexes: function (schemaName, tableName) { return `SELECT idx.relname as indexname, pg_get_indexdef(idx.oid) AS indexdef, d.description AS comment FROM pg_index i INNER JOIN pg_class tbl ON tbl.oid = i.indrelid INNER JOIN pg_namespace tbln ON tbl.relnamespace = tbln.oid INNER JOIN pg_class idx ON idx.oid = i.indexrelid LEFT JOIN pg_description d ON d.objoid = idx."oid" AND d.objsubid = 0 WHERE tbln.nspname = '${schemaName}' AND tbl.relname='${tableName}' AND i.indisprimary = false AND i.indisunique = false`; }, /** * * @param {String} schemaName * @param {String} tableName */ getTablePrivileges: function (schemaName, tableName) { return `SELECT t.schemaname, t.tablename, r.rolname, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'SELECT') as select, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'INSERT') as insert, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'UPDATE') as update, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'DELETE') as delete, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'TRUNCATE') as truncate, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'REFERENCES') as references, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${tableName}"', 'TRIGGER') as trigger FROM pg_tables t, pg_roles r WHERE t.schemaname = '${schemaName}' and t.tablename = '${tableName}'`; }, /** * * @param {String} schemaName * @param {String} tableName */ getTableTriggers: function (schemaName, tableName) { return `SELECT tgname as triggername, pg_get_triggerdef(t.oid) AS triggerdef, p.proname AS functionname, d.description AS comment FROM pg_trigger t INNER JOIN pg_class c ON c.oid = t.tgrelid INNER JOIN pg_namespace n ON n.oid = c.relnamespace INNER JOIN pg_proc p ON p.oid = t.tgfoid LEFT JOIN pg_description d ON d.objoid = t.oid AND d.objsubid = 0 WHERE n.nspname = '${schemaName}' AND c.relname = '${tableName}' AND t.tgisinternal = false `; }, /** * * @param {String[]} schemas */ getViews: function (schemas) { return `SELECT v.schemaname, v.viewname, v.viewowner, v.definition, d.description AS comment FROM pg_views v INNER JOIN pg_namespace n ON v.schemaname = n.nspname INNER JOIN pg_class c ON v.viewname = c.relname AND c.relnamespace = n."oid" LEFT JOIN pg_description d ON d.objoid = c."oid" AND d.objsubid = 0 WHERE v.schemaname IN ('${schemas.join("','")}') AND c.oid NOT IN ( SELECT d.objid FROM pg_depend d WHERE d.deptype = 'e' )`; }, /** * * @param {String} schemaName * @param {String} viewName */ getViewPrivileges: function (schemaName, viewName) { return `SELECT v.schemaname, v.viewname, r.rolname, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'SELECT') as select, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'INSERT') as insert, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'UPDATE') as update, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'DELETE') as delete, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'TRUNCATE') as truncate, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'REFERENCES') as references, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'TRIGGER') as trigger FROM pg_views v, pg_roles r WHERE v.schemaname = '${schemaName}' and v.viewname='${viewName}'`; }, /** * * @param {String[]} schemas */ getMaterializedViews: function (schemas) { return `SELECT m.schemaname, m.matviewname, m.matviewowner, m.definition, d.description AS comment FROM pg_matviews m INNER JOIN pg_namespace n ON m.schemaname = n.nspname INNER JOIN pg_class c ON m.matviewname = c.relname AND c.relnamespace = n."oid" LEFT JOIN pg_description d ON d.objoid = c."oid" AND d.objsubid = 0 WHERE schemaname IN ('${schemas.join("','")}')`; }, /** * * @param {String} schemaName * @param {String} viewName */ getMaterializedViewPrivileges: function (schemaName, viewName) { return `SELECT v.schemaname, v.matviewname, r.rolname, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'SELECT') as select, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'INSERT') as insert, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'UPDATE') as update, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'DELETE') as delete, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'TRUNCATE') as truncate, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'REFERENCES') as references, HAS_TABLE_PRIVILEGE(r.rolname,'"${schemaName}"."${viewName}"', 'TRIGGER') as trigger FROM pg_matviews v, pg_roles r WHERE v.schemaname = '${schemaName}' and v.matviewname='${viewName}'`; }, /** * * @param {String} schemaName * @param {String} viewName */ getViewDependencies: function (schemaName, viewName) { return `SELECT n.nspname AS schemaname, c.relname AS tablename, a.attname AS columnname FROM pg_rewrite AS r INNER JOIN pg_depend AS d ON r.oid=d.objid INNER JOIN pg_attribute a ON a.attnum = d.refobjsubid AND a.attrelid = d.refobjid AND a.attisdropped = false INNER JOIN pg_class c ON c.oid = d.refobjid INNER JOIN pg_namespace n ON n.oid = c.relnamespace INNER JOIN pg_namespace vn ON vn.nspname = '${schemaName}' INNER JOIN pg_class vc ON vc.relname = '${viewName}' AND vc.relnamespace = vn."oid" WHERE r.ev_class = vc.oid AND d.refobjid <> vc.oid`; }, /** * * @param {String[]} schemas * @param {import("../models/serverVersion")} serverVersion */ getFunctions: function (schemas, serverVersion) { //TODO: Instead of using ::regrole casting, for better performance join with pg_roles return `SELECT p.proname, n.nspname, pg_get_functiondef(p.oid) as definition, p.proowner::regrole::name as owner, oidvectortypes(proargtypes) as argtypes, d.description AS comment, ${core.checkServerCompatibility(serverVersion, 11, 0) ? "p.prokind" : "'f' as prokind"} FROM pg_proc p INNER JOIN pg_namespace n ON n.oid = p.pronamespace LEFT JOIN pg_description d ON d.objoid = p."oid" AND d.objsubid = 0 WHERE n.nspname IN ('${schemas.join("','")}') AND p.probin IS NULL ${core.checkServerCompatibility(serverVersion, 11, 0) ? "AND p.prokind IN ('f','p')" : "AND p.proisagg = false AND p.proiswindow = false"} AND p."oid" NOT IN ( SELECT d.objid FROM pg_depend d WHERE d.deptype = 'e' )`; }, /** * * @param {String[]} schemas * @param {import("../models/serverVersion")} serverVersion */ getAggregates: function (schemas, serverVersion) { //TODO: Instead of using ::regrole casting, for better performance join with pg_roles return `SELECT p.proname, n.nspname, p.proowner::regrole::name as owner, oidvectortypes(p.proargtypes) as argtypes, format('%s', array_to_string( ARRAY[ format(E'\\tSFUNC = %s', a.aggtransfn::text) , format(E'\\tSTYPE = %s', format_type(a.aggtranstype, NULL)) , format(E'\\tSSPACE = %s',a.aggtransspace) , CASE a.aggfinalfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tFINALFUNC = %s',a.aggfinalfn::text) END , CASE WHEN a.aggfinalfn != '-'::regproc AND a.aggfinalextra = true THEN format(E'\\tFINALFUNC_EXTRA') ELSE NULL END ${ core.checkServerCompatibility(serverVersion, 11, 0) ? `, CASE WHEN a.aggfinalfn != '-'::regproc THEN format(E'\\tFINALFUNC_MODIFY = %s', CASE WHEN a.aggfinalmodify = 'r' THEN 'READ_ONLY' WHEN a.aggfinalmodify = 's' THEN 'SHAREABLE' WHEN a.aggfinalmodify = 'w' THEN 'READ_WRITE' END ) ELSE NULL END` : "" } , CASE WHEN a.agginitval IS NULL THEN NULL ELSE format(E'\\tINITCOND = %s', a.agginitval) END , format(E'\\tPARALLEL = %s', CASE WHEN p.proparallel = 'u' THEN 'UNSAFE' WHEN p.proparallel = 's' THEN 'SAFE' WHEN p.proparallel = 'r' THEN 'RESTRICTED' END ) , CASE a.aggcombinefn WHEN '-'::regproc THEN NULL ELSE format(E'\\tCOMBINEFUNC = %s',a.aggcombinefn::text) END , CASE a.aggserialfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tSERIALFUNC = %s',a.aggserialfn::text) END , CASE a.aggdeserialfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tDESERIALFUNC = %s',a.aggdeserialfn::text) END , CASE a.aggmtransfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tMSFUNC = %s',a.aggmtransfn::text) END , case a.aggmtranstype WHEN '-'::regtype THEN NULL ELSE format(E'\\tMSTYPE = %s', format_type(a.aggmtranstype, NULL)) END , case WHEN a.aggmfinalfn != '-'::regproc THEN format(E'\\tMSSPACE = %s',a.aggmtransspace) ELSE NULL END , CASE a.aggminvtransfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tMINVFUNC = %s',a.aggminvtransfn::text) END , CASE a.aggmfinalfn WHEN '-'::regproc THEN NULL ELSE format(E'\\tMFINALFUNC = %s',a.aggmfinalfn::text) END , CASE WHEN a.aggmfinalfn != '-'::regproc and a.aggmfinalextra = true THEN format(E'\\tMFINALFUNC_EXTRA') ELSE NULL END ${ core.checkServerCompatibility(serverVersion, 11, 0) ? `, CASE WHEN a.aggmfinalfn != '-'::regproc THEN format(E'\\tMFINALFUNC_MODIFY = %s', CASE WHEN a.aggmfinalmodify = 'r' THEN 'READ_ONLY' WHEN a.aggmfinalmodify = 's' THEN 'SHAREABLE' WHEN a.aggmfinalmodify = 'w' THEN 'READ_WRITE' END ) ELSE NULL END` : "" } , CASE WHEN a.aggminitval IS NULL THEN NULL ELSE format(E'\\tMINITCOND = %s', a.aggminitval) END , CASE a.aggsortop WHEN 0 THEN NULL ELSE format(E'\\tSORTOP = %s', o.oprname) END ] , E',\\n' ) ) as definition, d.description AS comment FROM pg_proc p INNER JOIN pg_namespace n ON n.oid = p.pronamespace INNER JOIN pg_aggregate a on p.oid = a.aggfnoid LEFT JOIN pg_operator o ON o.oid = a.aggsortop LEFT JOIN pg_description d ON d.objoid = p."oid" AND d.objsubid = 0 WHERE n.nspname IN ('${schemas.join("','")}') AND a.aggkind = 'n' ${core.checkServerCompatibility(serverVersion, 11, 0) ? " AND p.prokind = 'a' " : " AND p.proisagg = true AND p.proiswindow = false "} AND p."oid" NOT IN ( SELECT d.objid FROM pg_depend d WHERE d.deptype = 'e' )`; }, /** * * @param {String} schemaName * @param {String} functionName * @param {String} argTypes */ getFunctionPrivileges: function (schemaName, functionName, argTypes) { return `SELECT g.specific_schema AS pronamespace, g.routine_name AS proname, g.grantee AS rolname, TRUE AS execute FROM pg_proc p, information_schema.role_routine_grants g WHERE g.specific_schema = '${schemaName}' AND p.proname = '${functionName}' AND oidvectortypes(p.proargtypes) = '${argTypes}' AND g.grantee != 'PUBLIC' AND g.privilege_type = 'EXECUTE' AND g.specific_name = CONCAT(p.proname,'_',p.oid)`; }, /** * * @param {String[]} schemas * @param {import("../models/serverVersion")} serverVersion */ getSequences: function (schemas, serverVersion) { return `SELECT s.seq_nspname, s.seq_name, s.owner, s.ownedby_table, s.ownedby_column, p.start_value, p.minimum_value, p.maximum_value, p.increment, p.cycle_option, ${core.checkServerCompatibility(serverVersion, 10, 0) ? "p.cache_size" : "1 as cache_size"}, s.comment FROM ( SELECT c.oid, ns.nspname AS seq_nspname, c.relname AS seq_name, r.rolname as owner, sc.relname AS ownedby_table, a.attname AS ownedby_column, ds.description AS comment FROM pg_class c INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace INNER JOIN pg_roles r ON r.oid = c.relowner LEFT JOIN pg_depend d ON d.objid = c.oid AND d.refobjsubid > 0 AND d.deptype ='a' LEFT JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid LEFT JOIN pg_class sc ON sc."oid" = d.refobjid LEFT JOIN pg_description ds ON ds.objoid = c."oid" AND ds.objsubid = 0 WHERE c.relkind = 'S' AND ns.nspname IN ('${schemas.join("','")}') ${core.checkServerCompatibility(serverVersion, 10, 0) ? "AND (a.attidentity IS NULL OR a.attidentity = '')" : ""} ) s, LATERAL pg_sequence_parameters(s.oid) p`; }, /** * * @param {String} schemaName * @param {String} sequenceName * @param {import("../models/serverVersion")} serverVersion */ getSequencePrivileges: function (schemaName, sequenceName, serverVersion) { return `SELECT s.sequence_schema, s.sequence_name, r.rolname, ${ core.checkServerCompatibility(serverVersion, 10, 0) ? "NULL AS cache_value," : "p.cache_value," } HAS_SEQUENCE_PRIVILEGE(r.rolname,'"${schemaName}"."${sequenceName}"', 'SELECT') as select, HAS_SEQUENCE_PRIVILEGE(r.rolname,'"${schemaName}"."${sequenceName}"', 'USAGE') as usage, HAS_SEQUENCE_PRIVILEGE(r.rolname,'"${schemaName}"."${sequenceName}"', 'UPDATE') as update FROM information_schema.sequences s, pg_roles r ${ core.checkServerCompatibility(serverVersion, 10, 0) ? "" : ', "' + schemaName + '"."' + sequenceName + '" p' } WHERE s.sequence_schema = '${schemaName}' and s.sequence_name='${sequenceName}'`; }, getAvailableExtensions: function () { return `SELECT name, installed_version FROM pg_available_extensions`; }, }; class CatalogApi { /** * * @param {import("pg").Client} client * @param {String[]} schemas */ static async retrieveAllSchemas(client) { /** @type {String[]} */ let result = []; /** @type {import("pg").QueryResult<any>} */ const namespaces = await client.query(query.getAllSchemas()); await Promise.all( namespaces.rows.map(async (namespace) => { result.push(namespace.nspname); }) ); return result; } /** * * @param {import("pg").Client} client * @param {String[]} schemas */ static async retrieveSchemas(client, schemas) { let result = {}; const namespaces = await client.query(query.getSchemas(schemas)); await Promise.all( namespaces.rows.map( async ( /** @type {{nspname:String, owner:String, comment: String}} */ namespace ) => { result[namespace.nspname] = { owner: namespace.owner, comment: namespace.comment, }; } ) ); return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveTables(client, config) { let result = {}; let tableNamesPriority = []; const tables = await client.query(query.getTables(config.compareOptions.schemaCompare.namespaces)); await Promise.all( tables.rows.map(async (table) => { const fullTableName = `"${table.schemaname}"."${table.tablename}"`; result[fullTableName] = { columns: {}, constraints: {}, options: {}, indexes: {}, privileges: {}, triggers: {}, owner: table.tableowner, comment: table.comment, }; const columns = await client.query(query.getTableColumns(table.schemaname, table.tablename, client.version)); columns.rows.forEach((column) => { let columnName = `"${column.attname}"`; let columnIdentity = null; let defaultValue = column.adsrc; let dataType = column.typname; let generatedColumn = null; switch (column.attidentity) { case "a": columnIdentity = "ALWAYS"; defaultValue = ""; break; case "d": columnIdentity = "BY DEFAULT"; defaultValue = ""; break; } switch (column.attgenerated) { case "s": generatedColumn = "STORED"; break; } result[fullTableName].columns[columnName] = { nullable: !column.attnotnull, datatype: dataType, dataTypeID: column.typeid, dataTypeCategory: column.typcategory, default: defaultValue, precision: column.precision, scale: column.scale, identity: columnIdentity, comment: column.comment, generatedColumn: generatedColumn, }; }); let constraints = await client.query(query.getTableConstraints(table.schemaname, table.tablename)); constraints.rows.forEach((constraint) => { let constraintName = `"${constraint.conname}"`; result[fullTableName].constraints[constraintName] = { type: constraint.contype, definition: constraint.definition, comment: constraint.comment, foreign_schema: constraint.foreign_schema, foreign_table: constraint.foreign_table, }; //REFERENCED tables have to be created before if (constraint.contype == "f") { const tableNameToReorder = `"${constraint.foreign_schema}"."${constraint.foreign_table}"`; const indexOfTableNameToReorder = tableNamesPriority.indexOf(tableNameToReorder); const indexOfCurrentTableName = tableNamesPriority.indexOf(fullTableName); if (indexOfCurrentTableName >= 0) { if (indexOfTableNameToReorder < 0) { tableNamesPriority.splice(indexOfCurrentTableName, 0, tableNameToReorder); } else if (indexOfCurrentTableName < indexOfTableNameToReorder) { tableNamesPriority.splice(indexOfCurrentTableName, 0, tableNamesPriority.splice(indexOfTableNameToReorder, 1)[0]); } } else if (indexOfTableNameToReorder < 0) { tableNamesPriority.push(`"${constraint.foreign_schema}"."${constraint.foreign_table}"`); } } }); //@mso -> relhadoids has been deprecated from PG v12.0 if (!core.checkServerCompatibility(client.version, 12, 0)) { let options = await client.query(query.getTableOptions(table.schemaname, table.tablename)); options.rows.forEach((option) => { result[fullTableName].options = { withOids: option.relhasoids, }; }); } let indexes = await client.query(query.getTableIndexes(table.schemaname, table.tablename)); indexes.rows.forEach((index) => { result[fullTableName].indexes[index.indexname] = { definition: index.indexdef, comment: index.comment, schema: table.schemaname, }; }); let privileges = await client.query(query.getTablePrivileges(table.schemaname, table.tablename)); privileges.rows.forEach((privilege) => { if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullTableName].privileges[privilege.rolname] = { select: privilege.select, insert: privilege.insert, update: privilege.update, delete: privilege.delete, truncate: privilege.truncate, references: privilege.references, trigger: privilege.trigger, }; }); let triggers = await client.query(query.getTableTriggers(table.schemaname, table.tablename)); triggers.rows.forEach((trigger) => { result[fullTableName].triggers[trigger.triggername] = { definition: trigger.triggerdef, function_name: trigger.functionname, comment: trigger.comment, }; }); //TODO: Missing discovering of PARTITION //TODO: Missing discovering of GRANTS for COLUMNS //TODO: Missing discovering of WITH GRANT OPTION, that is used to indicate if user\role can add GRANTS to other users }) ); //Re-order tables based on priority const reorderedResult = {}; for (const tableName of tableNamesPriority) { reorderedResult[tableName] = result[tableName]; delete result[tableName]; } result = { ...reorderedResult, ...result }; return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveViews(client, config) { let result = {}; //Get views const views = await client.query(query.getViews(config.compareOptions.schemaCompare.namespaces)); await Promise.all( views.rows.map(async (view) => { const fullViewName = `"${view.schemaname}"."${view.viewname}"`; result[fullViewName] = { definition: view.definition, owner: view.viewowner, privileges: {}, dependencies: [], comment: view.comment, }; let privileges = await client.query(query.getViewPrivileges(view.schemaname, view.viewname)); privileges.rows.forEach((privilege) => { if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullViewName].privileges[privilege.rolname] = { select: privilege.select, insert: privilege.insert, update: privilege.update, delete: privilege.delete, truncate: privilege.truncate, references: privilege.references, trigger: privilege.trigger, }; }); let dependencies = await client.query(query.getViewDependencies(view.schemaname, view.viewname)); dependencies.rows.forEach((dependency) => { result[fullViewName].dependencies.push({ schemaName: dependency.schemaname, tableName: dependency.tablename, columnName: dependency.columnname, }); }); }) ); //TODO: Missing discovering of TRIGGER //TODO: Missing discovering of GRANTS for COLUMNS //TODO: Should we get TEMPORARY VIEW? return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveMaterializedViews(client, config) { let result = {}; const views = await client.query(query.getMaterializedViews(config.compareOptions.schemaCompare.namespaces)); await Promise.all( views.rows.map(async (view) => { const fullViewName = `"${view.schemaname}"."${view.matviewname}"`; result[fullViewName] = { definition: view.definition, indexes: {}, owner: view.matviewowner, privileges: {}, dependencies: [], comment: view.comment, }; let indexes = await client.query(query.getTableIndexes(view.schemaname, view.matviewname)); indexes.rows.forEach((index) => { result[fullViewName].indexes[index.indexname] = { definition: index.indexdef, comment: index.comment, schema: view.schemaname, }; }); let privileges = await client.query(query.getMaterializedViewPrivileges(view.schemaname, view.matviewname)); privileges.rows.forEach((privilege) => { if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullViewName].privileges[privilege.rolname] = { select: privilege.select, insert: privilege.insert, update: privilege.update, delete: privilege.delete, truncate: privilege.truncate, references: privilege.references, trigger: privilege.trigger, }; }); let dependencies = await client.query(query.getViewDependencies(view.schemaname, view.matviewname)); dependencies.rows.forEach((dependency) => { result[fullViewName].dependencies.push({ schemaName: dependency.schemaname, tableName: dependency.tablename, columnName: dependency.columnname, }); }); }) ); //TODO: Missing discovering of GRANTS for COLUMNS return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveFunctions(client, config) { let result = {}; const procedures = await client.query(query.getFunctions(config.compareOptions.schemaCompare.namespaces, client.version)); await Promise.all( procedures.rows.map(async (procedure) => { let fullProcedureName = `"${procedure.nspname}"."${procedure.proname}"`; if (!result[fullProcedureName]) result[fullProcedureName] = {}; result[fullProcedureName][procedure.argtypes] = { definition: procedure.definition, owner: procedure.owner, argTypes: procedure.argtypes, privileges: {}, comment: procedure.comment, type: procedure.prokind, }; let privileges = await client.query(query.getFunctionPrivileges(procedure.nspname, procedure.proname, procedure.argtypes)); privileges.rows.forEach((privilege) => { if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullProcedureName][procedure.argtypes].privileges[privilege.rolname] = { execute: privilege.execute, }; }); }) ); return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveAggregates(client, config) { let result = {}; const aggregates = await client.query(query.getAggregates(config.compareOptions.schemaCompare.namespaces, client.version)); await Promise.all( aggregates.rows.map(async (aggregate) => { let fullAggregateName = `"${aggregate.nspname}"."${aggregate.proname}"`; if (!result[fullAggregateName]) result[fullAggregateName] = {}; result[fullAggregateName][aggregate.argtypes] = { definition: aggregate.definition, owner: aggregate.owner, argTypes: aggregate.argtypes, privileges: {}, comment: aggregate.comment, }; let privileges = await client.query(query.getFunctionPrivileges(aggregate.nspname, aggregate.proname, aggregate.argtypes)); privileges.rows.forEach((privilege) => { if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullAggregateName][aggregate.argtypes].privileges[privilege.rolname] = { execute: privilege.execute, }; }); }) ); return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveSequences(client, config) { let result = {}; const sequences = await client.query(query.getSequences(config.compareOptions.schemaCompare.namespaces, client.version)); await Promise.all( sequences.rows.map(async (sequence) => { let fullSequenceName = `"${sequence.seq_nspname}"."${sequence.seq_name}"`; result[fullSequenceName] = { owner: sequence.owner, startValue: sequence.start_value, minValue: sequence.minimum_value, maxValue: sequence.maximum_value, increment: sequence.increment, cacheSize: sequence.cache_size, isCycle: sequence.cycle_option, name: sequence.seq_name, ownedBy: sequence.ownedby_table && sequence.ownedby_column ? `${sequence.ownedby_table}.${sequence.ownedby_column}` : null, privileges: {}, comment: sequence.comment, }; let privileges = await client.query(query.getSequencePrivileges(sequence.seq_nspname, sequence.seq_name, client.version)); privileges.rows.forEach((privilege) => { if (privilege.cache_value != null) result[fullSequenceName].cacheSize = privilege.cache_value; if ( config.compareOptions.schemaCompare.roles.length <= 0 || config.compareOptions.schemaCompare.roles.includes(privilege.rolname) ) result[fullSequenceName].privileges[privilege.rolname] = { select: privilege.select, usage: privilege.usage, update: privilege.update, }; }); }) ); return result; } /** * * @param {import("pg").Client} client * @param {import("../models/config")} config */ static async retrieveExtensions(client) { let result = {}; const extensions = await client.query(query.getAvailableExtensions()); await Promise.all( extensions.rows.map( async ( /** @type {{name:String, installed_version:String}} */ extension ) => { result[extension.name] = { version: extension.installed_version, }; } ) ); return result; } } module.exports = CatalogApi;