diff --git a/integration-tests/__tests__/object-analyse.spec.js b/integration-tests/__tests__/object-analyse.spec.js index f924f2137..8a8956dd5 100644 --- a/integration-tests/__tests__/object-analyse.spec.js +++ b/integration-tests/__tests__/object-analyse.spec.js @@ -19,6 +19,10 @@ function flatSourceParameters() { ); } +function flatSourceTriggers() { + return _.flatten(engines.map(engine => (engine.triggers || []).map(trigger => [engine.label, trigger, engine]))); +} + const obj1Match = expect.objectContaining({ pureName: 'obj1', }); @@ -136,4 +140,26 @@ describe('Object analyse', () => { } }) ); + + test.each(flatSourceTriggers())( + 'Test triggers - %s - %s', + testWrapper(async (conn, driver, trigger) => { + for (const sql of initSql) await runCommandOnDriver(conn, driver, sql); + + const { triggerOtherDropSql, triggerOtherCreateSql, create, drop, expected, objectTypeField } = trigger; + + if (triggerOtherCreateSql) await runCommandOnDriver(conn, driver, triggerOtherCreateSql); + + await runCommandOnDriver(conn, driver, create); + const structure = await driver.analyseFull(conn); + await runCommandOnDriver(conn, driver, drop); + + if (triggerOtherDropSql) await runCommandOnDriver(conn, driver, triggerOtherDropSql); + + const createdTrigger = structure[objectTypeField].find(x => x.pureName == expected.pureName); + expect(createdTrigger).toEqual(expect.objectContaining(expected)); + }) + ); }); + +console.log(flatSourceTriggers()); diff --git a/integration-tests/engines.js b/integration-tests/engines.js index da452af83..a9bd1bc9b 100644 --- a/integration-tests/engines.js +++ b/integration-tests/engines.js @@ -108,6 +108,30 @@ const engines = [ ], }, ], + triggers: [ + { + testName: 'triggers insert after', + create: 'CREATE TRIGGER obj1 AFTER INSERT ON t1 FOR EACH ROW BEGIN END', + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'INSERT', + triggerTiming: 'AFTER', + }, + }, + { + testName: 'triggers insert before', + create: 'CREATE TRIGGER obj1 BEFORE INSERT ON t1 FOR EACH ROW BEGIN END', + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'INSERT', + triggerTiming: 'BEFORE', + }, + }, + ], }, { label: 'MariaDB', @@ -264,6 +288,50 @@ end;$$`, ], }, ], + triggers: [ + { + testName: 'triggers after each row', + create: `CREATE TRIGGER obj1 +AFTER INSERT ON t1 +FOR EACH ROW +EXECUTE FUNCTION test_function(); +`, + drop: 'DROP TRIGGER obj1 ON t1;', + triggerOtherCreateSql: `CREATE OR REPLACE FUNCTION test_function() +RETURNS TRIGGER AS $$ +BEGIN +END; +$$ LANGUAGE plpgsql;`, + triggerOtherDropSql: 'DROP FUNCTION test_function', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'INSERT', + triggerTiming: 'AFTER', + }, + }, + { + testName: 'triggers before each row', + create: `CREATE TRIGGER obj1 +BEFORE INSERT ON t1 +FOR EACH ROW +EXECUTE FUNCTION test_function(); +`, + drop: 'DROP TRIGGER obj1 ON t1;', + triggerOtherCreateSql: `CREATE OR REPLACE FUNCTION test_function() +RETURNS TRIGGER AS $$ +BEGIN +END; +$$ LANGUAGE plpgsql;`, + triggerOtherDropSql: 'DROP FUNCTION test_function', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'INSERT', + triggerTiming: 'BEFORE', + }, + }, + ], }, { label: 'SQL Server', @@ -349,6 +417,42 @@ end;$$`, supportRenameSqlObject: true, defaultSchemaName: 'dbo', // skipSeparateSchemas: true, + triggers: [ + { + testName: 'triggers before each row', + create: `CREATE TRIGGER obj1 +ON t1 +AFTER INSERT +AS +BEGIN +SELECT * FROM t1 +END;`, + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'INSERT', + triggerTiming: 'AFTER', + }, + }, + { + testName: 'triggers before each row', + create: `CREATE TRIGGER obj1 +ON t1 +AFTER UPDATE +AS +BEGIN +SELECT * FROM t1 +END;`, + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'obj1', + eventType: 'UPDATE', + triggerTiming: 'AFTER', + }, + }, + ], }, { label: 'SQLite', @@ -429,25 +533,51 @@ end;$$`, }, { type: 'functions', - create1: 'CREATE FUNCTION ~obj1 RETURN NUMBER IS v_count NUMBER; \n BEGIN SELECT COUNT(*) INTO v_count FROM ~t1;\n RETURN v_count;\n END ~obj1', - create2: 'CREATE FUNCTION ~obj2 RETURN NUMBER IS v_count NUMBER; \n BEGIN SELECT COUNT(*) INTO v_count FROM ~t2;\n RETURN v_count;\n END ~obj2', + create1: + 'CREATE FUNCTION ~obj1 RETURN NUMBER IS v_count NUMBER; \n BEGIN SELECT COUNT(*) INTO v_count FROM ~t1;\n RETURN v_count;\n END ~obj1', + create2: + 'CREATE FUNCTION ~obj2 RETURN NUMBER IS v_count NUMBER; \n BEGIN SELECT COUNT(*) INTO v_count FROM ~t2;\n RETURN v_count;\n END ~obj2', drop1: 'DROP FUNCTION ~obj1', drop2: 'DROP FUNCTION ~obj2', }, ], + triggers: [ + { + testName: 'triggers after each row', + create: 'CREATE OR REPLACE TRIGGER obj1 AFTER INSERT ON "t1" FOR EACH ROW BEGIN END obj1;', + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'OBJ1', + eventType: 'INSERT', + triggerTiming: 'AFTER EACH ROW', + }, + }, + { + testName: 'triggers before each row', + create: 'CREATE OR REPLACE TRIGGER obj1 BEFORE INSERT ON "t1" FOR EACH ROW BEGIN END obj1;', + drop: 'DROP TRIGGER obj1;', + objectTypeField: 'triggers', + expected: { + pureName: 'OBJ1', + eventType: 'INSERT', + triggerTiming: 'BEFORE EACH ROW', + }, + }, + ], }, ]; const filterLocal = [ // filter local testing '-MySQL', - '-MariaDB', + // '-MariaDB', '-PostgreSQL', - '-SQL Server', - '-SQLite', - '-CockroachDB', - '-ClickHouse', - 'Oracle', + 'SQL Server', + // '-SQLite', + // '-CockroachDB', + // '-ClickHouse', + // 'Oracle', ]; const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL'); diff --git a/packages/tools/src/yamlModelConv.ts b/packages/tools/src/yamlModelConv.ts index da4fe5219..4ed6e3b9b 100644 --- a/packages/tools/src/yamlModelConv.ts +++ b/packages/tools/src/yamlModelConv.ts @@ -234,6 +234,7 @@ export function databaseInfoFromYamlModel(filesOrDbInfo: DatabaseModelFile[] | D if (file.name.endsWith('.trigger.sql')) { model.triggers.push({ + objectId: `triggers:${file.name.slice(0, -'.trigger.sql'.length)}`, pureName: file.name.slice(0, -'.trigger.sql'.length), createSql: file.text, }); diff --git a/packages/types/dbinfo.d.ts b/packages/types/dbinfo.d.ts index 3a50fbbfd..d5b736561 100644 --- a/packages/types/dbinfo.d.ts +++ b/packages/types/dbinfo.d.ts @@ -136,7 +136,25 @@ export interface FunctionInfo extends CallableObjectInfo { returnType?: string; } -export interface TriggerInfo extends SqlObjectInfo {} +export interface TriggerInfo extends SqlObjectInfo { + objectId: string; + functionName?: string; + tableName?: string; + triggerTiming?: + | 'BEFORE' + | 'AFTER' + | 'INSTEAD OF' + | 'BEFORE EACH ROW' + | 'INSTEAD OF' + | 'AFTER EACH ROW' + | 'AFTER STATEMENT' + | 'BEFORE STATEMENT' + | 'AFTER EVENT' + | 'BEFORE EVENT' + | null; + triggerLevel?: 'ROW' | 'STATEMENT'; + eventType?: 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE'; +} export interface SchemaInfo { objectId?: string; diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index 7a65b57d2..755b2769a 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -41,6 +41,7 @@ procedures: 'img procedure', functions: 'img function', queries: 'img query-data', + triggers: 'icon trigger', }; const defaultTabs = { @@ -51,6 +52,7 @@ queries: 'QueryDataTab', procedures: 'SqlObjectTab', functions: 'SqlObjectTab', + triggers: 'SqlObjectTab', }; function createScriptTemplatesSubmenu(objectTypeField) { @@ -342,40 +344,8 @@ }, ]; case 'functions': - return [ - ...defaultDatabaseObjectAppObjectActions['functions'], - { - divider: true, - }, - hasPermission('dbops/model/edit') && { - label: 'Drop function', - isDrop: true, - requiresWriteAccess: true, - }, - hasPermission('dbops/model/edit') && { - label: 'Rename function', - isRename: true, - requiresWriteAccess: true, - }, - createScriptTemplatesSubmenu('functions'), - { - label: 'SQL generator', - submenu: [ - { - label: 'CREATE FUNCTION', - sqlGeneratorProps: { - createFunctions: true, - }, - }, - { - label: 'DROP FUNCTION', - sqlGeneratorProps: { - dropFunctions: true, - }, - }, - ], - }, - ]; + case 'triggers': + return [...defaultDatabaseObjectAppObjectActions['triggers']]; case 'collections': return [ ...defaultDatabaseObjectAppObjectActions['collections'], @@ -753,7 +723,7 @@ return; } - const availableDefaultActions = defaultDatabaseObjectAppObjectActions[objectTypeField]; + const availableDefaultActions = defaultDatabaseObjectAppObjectActions[objectTypeField] ?? []; const configuredActionId = getLastUsedDefaultActions()[objectTypeField]; const prefferedAction = @@ -979,6 +949,10 @@ function getExtInfo(data) { const res = []; + if (data.objectTypeField === 'triggers') { + res.push(`${data.triggerTiming ?? ''} ${data.eventType ?? ''}`.toLowerCase()); + } + if (data.objectComment) { res.push(data.objectComment); } diff --git a/packages/web/src/appobj/appObjectTools.ts b/packages/web/src/appobj/appObjectTools.ts index 1eb48e45f..7b3e0bb8c 100644 --- a/packages/web/src/appobj/appObjectTools.ts +++ b/packages/web/src/appobj/appObjectTools.ts @@ -60,6 +60,14 @@ export const defaultDatabaseObjectAppObjectActions = { icon: 'img sql-file', }, ], + triggers: [ + { + label: 'Show SQL', + tab: 'SqlObjectTab', + defaultActionId: 'showSql', + icon: 'img sql-file', + }, + ], collections: [ { label: 'Open data', diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 4cf0e103a..0f4a63012 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -64,6 +64,7 @@ 'icon add-folder': 'mdi mdi-folder-plus-outline', 'icon add-column': 'mdi mdi-table-column-plus-after', 'icon parameter': 'mdi mdi-at', + 'icon trigger': 'mdi mdi-lightning-bolt', 'icon window-restore': 'mdi mdi-window-restore', 'icon window-maximize': 'mdi mdi-window-maximize', diff --git a/packages/web/src/utility/applyScriptTemplate.ts b/packages/web/src/utility/applyScriptTemplate.ts index 124e32f21..ac2de4b0f 100644 --- a/packages/web/src/utility/applyScriptTemplate.ts +++ b/packages/web/src/utility/applyScriptTemplate.ts @@ -166,6 +166,13 @@ export function getSupportedScriptTemplates(objectTypeField: string): { label: s scriptTemplate: 'CALL OBJECT', }, ]; + case 'triggers': + return [ + { + label: 'CREATE TRIGGER', + scriptTemplate: 'CREATE OBJECT', + }, + ]; } return []; diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 20c1bb90b..e8001a4f7 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -80,7 +80,7 @@ // $: console.log('OBJECTS', $objects); $: objectList = _.flatten([ - ...['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField => + ...['tables', 'collections', 'views', 'matviews', 'procedures', 'functions', 'triggers'].map(objectTypeField => _.sortBy( (($objects || {})[objectTypeField] || []).map(obj => ({ ...obj, objectTypeField })), ['schemaName', 'pureName'] diff --git a/patches/ace-builds+1.33.1.patch b/patches/ace-builds+1.33.1.patch new file mode 100644 index 000000000..333bc1aaf --- /dev/null +++ b/patches/ace-builds+1.33.1.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/ace-builds/src-noconflict/mode-sqlserver.js b/node_modules/ace-builds/src-noconflict/mode-sqlserver.js +index 7e61d3d..3e0fb97 100644 +--- a/node_modules/ace-builds/src-noconflict/mode-sqlserver.js ++++ b/node_modules/ace-builds/src-noconflict/mode-sqlserver.js +@@ -304,7 +304,7 @@ var BaseFoldMode = require("./cstyle").FoldMode; + var FoldMode = exports.FoldMode = function () { }; + oop.inherits(FoldMode, BaseFoldMode); + (function () { +- this.foldingStartMarker = /(\bCASE\b|\bBEGIN\b)|^\s*(\/\*)/i; ++ this.foldingStartMarker = /(? ({ + objectId: `triggers:${row.objectId}`, + contentHash: row.modifyDate && row.modifyDate.toISOString(), + createSql: row.definition, + triggerTiming: row.triggerTiming, + eventType: row.eventType, + schemaName: row.schemaName, + tableName: row.tableName, + pureName: row.triggerName, + })); + this.feedback({ analysingMessage: null }); return { tables, views, procedures, functions, + triggers, }; } diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js index 6352b6a9b..4125b1ebd 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js @@ -12,6 +12,7 @@ const functionParameters = require('./functionParameters'); const viewColumns = require('./viewColumns'); const indexes = require('./indexes'); const indexcols = require('./indexcols'); +const triggers = require('./triggers'); module.exports = { columns, @@ -28,4 +29,5 @@ module.exports = { indexes, indexcols, tableSizes, + triggers, }; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/triggers.js b/plugins/dbgate-plugin-mssql/src/backend/sql/triggers.js new file mode 100644 index 000000000..3cf1dd1e8 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/triggers.js @@ -0,0 +1,28 @@ +module.exports = ` +SELECT + o.modify_date as modifyDate, + o.object_id as objectId, + o.name AS triggerName, + s.name AS schemaName, + OBJECT_NAME(o.parent_object_id) AS tableName, + CASE + WHEN OBJECTPROPERTY(o.object_id, 'ExecIsAfterTrigger') = 1 THEN 'AFTER' + WHEN OBJECTPROPERTY(o.object_id, 'ExecIsInsteadOfTrigger') = 1 THEN 'INSTEAD OF' + ELSE 'BEFORE' + END AS triggerTiming, + CASE + WHEN OBJECTPROPERTY(o.object_id, 'ExecIsInsertTrigger') = 1 THEN 'INSERT' + WHEN OBJECTPROPERTY(o.object_id, 'ExecIsUpdateTrigger') = 1 THEN 'UPDATE' + WHEN OBJECTPROPERTY(o.object_id, 'ExecIsDeleteTrigger') = 1 THEN 'DELETE' + END AS eventType, + OBJECT_DEFINITION(o.object_id) AS definition +FROM sys.objects o +INNER JOIN sys.tables t + ON o.parent_object_id = t.object_id +INNER JOIN sys.schemas s + ON t.schema_id = s.schema_id +WHERE o.type = 'TR' + AND o.is_ms_shipped = 0 + AND o.object_id =OBJECT_ID_CONDITION + AND s.name =SCHEMA_NAME_CONDITION +`; diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index 1ff963894..34b83a0ba 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -160,6 +160,10 @@ class Analyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'Loading indexes' }); const indexes = await this.analyserQuery('indexes', ['tables']); this.feedback({ analysingMessage: 'Loading uniques' }); + + this.feedback({ analysingMessage: 'Loading triggers' }); + const triggers = await this.analyserQuery('triggers'); + const uniqueNames = await this.analyserQuery('uniqueNames', ['tables']); this.feedback({ analysingMessage: 'Finalizing DB structure' }); @@ -236,6 +240,15 @@ class Analyser extends DatabaseAnalyser { contentHash: _.isDate(x.modifyDate) ? x.modifyDate.toISOString() : x.modifyDate, parameters: functionNameToParameters[x.pureName], })), + triggers: triggers.rows.map(row => ({ + contentHash: row.modifyDate, + pureName: row.triggerName, + eventType: row.eventType, + triggerTiming: row.triggerTiming, + schemaName: row.schemaName, + tableName: row.tableName, + createSql: row.definition, + })), }; this.feedback({ analysingMessage: null }); return res; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js index 8c3e095fa..1a7eda8d7 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js @@ -11,6 +11,7 @@ const functionModifications = require('./functionModifications'); const uniqueNames = require('./uniqueNames'); const viewTexts = require('./viewTexts'); const parameters = require('./parameters'); +const triggers = require('./triggers'); module.exports = { columns, @@ -26,4 +27,5 @@ module.exports = { indexes, uniqueNames, viewTexts, + triggers, }; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/triggers.js b/plugins/dbgate-plugin-mysql/src/backend/sql/triggers.js new file mode 100644 index 000000000..f548d96e7 --- /dev/null +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/triggers.js @@ -0,0 +1,13 @@ +module.exports = ` +SELECT + TRIGGER_NAME AS triggerName, + EVENT_MANIPULATION AS eventType, + ACTION_TIMING AS triggerTiming, + EVENT_OBJECT_SCHEMA AS schemaName, + EVENT_OBJECT_TABLE AS tableName, + ACTION_STATEMENT AS definition, + CREATED as modifyDate +FROM + INFORMATION_SCHEMA.TRIGGERS + WHERE EVENT_OBJECT_SCHEMA = '#DATABASE#' AND TRIGGER_NAME =OBJECT_ID_CONDITION +`; diff --git a/plugins/dbgate-plugin-oracle/src/backend/Analyser.js b/plugins/dbgate-plugin-oracle/src/backend/Analyser.js index 397f7aa5f..b25676134 100644 --- a/plugins/dbgate-plugin-oracle/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-oracle/src/backend/Analyser.js @@ -71,6 +71,10 @@ class Analyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'Loading indexes' }); const indexes = await this.analyserQuery('indexes', ['tables'], { $owner: this.dbhan.database }); this.feedback({ analysingMessage: 'Loading unique names' }); + + const triggers = await this.analyserQuery('triggers', undefined, { $owner: this.dbhan.database }); + this.feedback({ analysingMessage: 'Loading triggers' }); + const uniqueNames = await this.analyserQuery('uniqueNames', ['tables'], { $owner: this.dbhan.database }); this.feedback({ analysingMessage: 'Finalizing DB structure' }); @@ -183,6 +187,15 @@ class Analyser extends DatabaseAnalyser { // schemaName: func.schema_name, contentHash: func.hash_code, })), + triggers: triggers.rows.map(row => ({ + pureName: row.TRIGGER_NAME, + trigerName: row.TRIGGER_NAME, + definition: row.DEFINITION, + tableName: row.TABLE_NAME, + triggerLevel: row.TRIGGER_LEVEL, + triggerTiming: row.TRIGGER_TIMING, + eventType: row.EVENT_TYPE, + })), }; this.feedback({ analysingMessage: null }); diff --git a/plugins/dbgate-plugin-oracle/src/backend/sql/index.js b/plugins/dbgate-plugin-oracle/src/backend/sql/index.js index ca1b3f24f..70e7bf59c 100644 --- a/plugins/dbgate-plugin-oracle/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-oracle/src/backend/sql/index.js @@ -6,6 +6,7 @@ const views = require('./views'); const matviews = require('./matviews'); const routines = require('./routines'); const indexes = require('./indexes'); // use mysql +const triggers = require('./triggers'); //const indexcols = require('./indexcols'); const uniqueNames = require('./uniqueNames'); //const geometryColumns = require('./geometryColumns'); @@ -24,7 +25,8 @@ module.exports = { routines, matviews, indexes, -// indexcols, + triggers, + // indexcols, uniqueNames, //geometryColumns, //geographyColumns, diff --git a/plugins/dbgate-plugin-oracle/src/backend/sql/triggers.js b/plugins/dbgate-plugin-oracle/src/backend/sql/triggers.js new file mode 100644 index 000000000..78517df06 --- /dev/null +++ b/plugins/dbgate-plugin-oracle/src/backend/sql/triggers.js @@ -0,0 +1,19 @@ +module.exports = ` +SELECT + TRIGGER_TYPE AS trigger_timing, + TRIGGERING_EVENT AS event_type, + TRIGGER_BODY AS definition, + TRIGGER_NAME AS trigger_name, + TABLE_NAME AS table_name, + OWNER, + CASE + WHEN INSTR(TRIGGER_TYPE, 'ROW') > 0 THEN 'ROW' + WHEN INSTR(TRIGGER_TYPE, 'STATEMENT') > 0 THEN 'STATEMENT' + ELSE NULL + END AS trigger_level +FROM + all_triggers +WHERE + OWNER='$owner' + AND 'tables:' || TABLE_NAME =OBJECT_ID_CONDITION +`; diff --git a/plugins/dbgate-plugin-postgres/src/backend/Analyser.js b/plugins/dbgate-plugin-postgres/src/backend/Analyser.js index 2823ade51..f545e2761 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-postgres/src/backend/Analyser.js @@ -114,13 +114,19 @@ class Analyser extends DatabaseAnalyser { // if (!cntBase || !cntRef) continue; const baseCols = _.sortBy( fk_keyColumnUsage.rows.filter( - x => x.table_name == fkRef.table_name && x.constraint_name == fkRef.constraint_name && x.table_schema == fkRef.table_schema + x => + x.table_name == fkRef.table_name && + x.constraint_name == fkRef.constraint_name && + x.table_schema == fkRef.table_schema ), 'ordinal_position' ); const refCols = _.sortBy( fk_keyColumnUsage.rows.filter( - x => x.table_name == fkRef.ref_table_name && x.constraint_name == fkRef.unique_constraint_name && x.table_schema == fkRef.ref_table_schema + x => + x.table_name == fkRef.ref_table_name && + x.constraint_name == fkRef.unique_constraint_name && + x.table_schema == fkRef.ref_table_schema ), 'ordinal_position' ); @@ -185,6 +191,9 @@ class Analyser extends DatabaseAnalyser { geographyColumns = await this.analyserQuery('geographyColumns', ['tables']); } + this.feedback({ analysingMessage: 'Loading triggers' }); + const triggers = await this.analyserQuery('triggers'); + this.feedback({ analysingMessage: 'Finalizing DB structure' }); const columnColumnsMapped = fkColumns.rows.map(x => ({ @@ -348,6 +357,19 @@ class Analyser extends DatabaseAnalyser { parameters: functionNameToParameters[`${func.schema_name}.${func.pure_name}`], returnType: func.data_type, })), + triggers: triggers.rows.map(row => ({ + pureName: row.trigger_name, + trigerName: row.trigger_name, + functionName: row.function_name, + triggerTiming: row.trigger_timing, + triggerLevel: row.trigger_level, + eventType: row.event_type, + schemaName: row.schema_name, + tableName: row.table_name, + createSql: row.definition, + contentHash: `triggers:${row.trigger_id}`, + objectId: `triggers:${row.trigger_id}`, + })), }; this.feedback({ analysingMessage: null }); diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js index e57203fe9..8f648c64f 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js @@ -16,6 +16,8 @@ const geometryColumns = require('./geometryColumns'); const geographyColumns = require('./geographyColumns'); const proceduresParameters = require('./proceduresParameters'); const foreignKeys = require('./foreignKeys'); +const triggers = require('./triggers'); + const fk_keyColumnUsage = require('./fk_key_column_usage'); module.exports = { @@ -38,4 +40,5 @@ module.exports = { geometryColumns, geographyColumns, proceduresParameters, + triggers, }; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/triggers.js b/plugins/dbgate-plugin-postgres/src/backend/sql/triggers.js new file mode 100644 index 000000000..f5afcd4b1 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/triggers.js @@ -0,0 +1,35 @@ +module.exports = ` +SELECT + t.oid AS trigger_id, + t.tgname AS trigger_name, + n.nspname AS schema_name, + c.relname AS table_name, + p.proname AS function_name, + t.tgtype AS original_tgtype, + CASE + WHEN t.tgtype & 1 = 1 THEN 'ROW' + ELSE 'STATEMENT' + END AS trigger_level, + COALESCE( + CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN NULL ELSE 'BEFORE' END, + CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN 'AFTER' ELSE NULL END, + CASE WHEN (tgtype::int::bit(7) & b'1000000')::int = 0 THEN NULL ELSE 'INSTEAD OF' END, + '' + )::text as trigger_timing, + (CASE WHEN (tgtype::int::bit(7) & b'0000100')::int = 0 THEN '' ELSE 'INSERT' END) || + (CASE WHEN (tgtype::int::bit(7) & b'0001000')::int = 0 THEN '' ELSE 'DELETE' END) || + (CASE WHEN (tgtype::int::bit(7) & b'0010000')::int = 0 THEN '' ELSE 'UPDATE' END) || + (CASE WHEN (tgtype::int::bit(7) & b'0100000')::int = 0 THEN '' ELSE 'TRUNCATE' END) + as event_type, + pg_get_triggerdef(t.oid) AS definition +FROM + pg_trigger t +JOIN + pg_class c ON c.oid = t.tgrelid +JOIN + pg_namespace n ON n.oid = c.relnamespace +JOIN + pg_proc p ON p.oid = t.tgfoid +WHERE + NOT t.tgisinternal AND n.nspname =SCHEMA_NAME_CONDITION +`;