Merge pull request #969 from dbgate/feature/triggers

Feature/triggers
This commit is contained in:
Jan Prochazka
2024-12-19 12:09:08 +01:00
committed by GitHub
22 changed files with 402 additions and 48 deletions

View File

@@ -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());

View File

@@ -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');

View File

@@ -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,
});

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -166,6 +166,13 @@ export function getSupportedScriptTemplates(objectTypeField: string): { label: s
scriptTemplate: 'CALL OBJECT',
},
];
case 'triggers':
return [
{
label: 'CREATE TRIGGER',
scriptTemplate: 'CREATE OBJECT',
},
];
}
return [];

View File

@@ -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']

View File

@@ -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 = /(?<!\[)(\bCASE\b|\bBEGIN\b)(?!\])|^\s*(\/\*)/i;
this.startRegionRe = /^\s*(\/\*|--)#?region\b/;
this.getFoldWidgetRange = function (session, foldStyle, row, forceMultiline) {
var line = session.getLine(row);
@@ -337,7 +337,7 @@ oop.inherits(FoldMode, BaseFoldMode);
var maxRow = session.getLength();
var line;
var depth = 1;
- var re = /(\bCASE\b|\bBEGIN\b)|(\bEND\b)/i;
+ var re = /(?<!\[)(\bCASE\b|\bBEGIN\b)|(\bEND\b)(?!\])/i;
while (++row < maxRow) {
line = session.getLine(row);
var m = re.exec(line);

View File

@@ -132,6 +132,9 @@ class MsSqlAnalyser extends DatabaseAnalyser {
const procedureParameterRows = await this.analyserQuery('proceduresParameters');
const functionParameterRows = await this.analyserQuery('functionParameters');
this.feedback({ analysingMessage: 'Loading triggers' });
const triggerRows = await this.analyserQuery('triggers');
this.feedback({ analysingMessage: 'Loading view columns' });
const viewColumnRows = await this.analyserQuery('viewColumns', ['views']);
@@ -214,12 +217,24 @@ class MsSqlAnalyser extends DatabaseAnalyser {
parameters: functionToParameters[row.objectId],
}));
const triggers = triggerRows.rows.map(row => ({
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,
};
}

View File

@@ -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,
};

View File

@@ -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
`;

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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
`;

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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
`;

View File

@@ -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 });

View File

@@ -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,
};

View File

@@ -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
`;