Building a routine to generate CREATE TRIGGER code in Postgres - postgresql

I haven't found a straightforward way to retrieve trigger definition code. I mean the trigger/binding declaration, not the trigger function. I figured I'd use some of the system catalogs to build up a script. The following, incomplete, version produces sensible output:
CREATE OR REPLACE FUNCTION dba.ddl_get_build_trigger_code(trigger_id oid) -- I starting from having the OID.
RETURNS text
AS $BODY$
DECLARE
trigger_name_in text;
code text;
BEGIN
/*
What the original declaration looks like:
CREATE TRIGGER trigger_hsys_after_delete
AFTER DELETE
ON data.hsys
REFERENCING OLD TABLE AS deleted_rows
FOR EACH STATEMENT
EXECUTE PROCEDURE data.trigger_function_log_deletion_count();
*/
SELECT tgname FROM pg_trigger WHERE oid = trigger_id INTO trigger_name_in; -- information_schema tables don't use PG OIDs.
RETURN
'CREATE TRIGGER ' || trigger_name || chr(10) ||
chr(9) || action_timing || ' ' || event_manipulation || chr(10) ||
chr(9) || 'ON ' || event_object_schema || '.' || event_object_table ||
CASE WHEN action_reference_old_table IS NOT NULL THEN
chr(10) || chr(9) || 'REFERENCING OLD TABLE AS ' || action_reference_old_table || chr(10) END ||
-- CASE WHEN action_reference_new_table IS NOT NULL THEN
-- chr(10) || chr(9) || 'REFERENCING NEW TABLE AS ' || action_reference_new_table || chr(10) END ||
chr(9) || 'FOR EACH ' || action_orientation || chr(10) ||
chr(9) || action_statement || ';' as create_trigger_code
FROM information_schema.triggers
WHERE trigger_name = trigger_name_in;
END;
$BODY$
LANGUAGE plpgsql;
Here's a sample, matching my actual case:
CREATE TRIGGER trigger_hsys_after_delete
AFTER DELETE
ON data.hsys
REFERENCING OLD TABLE AS deleted_rows
FOR EACH STATEMENT
EXECUTE PROCEDURE trigger_function_log_deletion_count();
There are several more attributes in information_schema.triggers that may have values, such as action_reference_new_table. When I enable the lines below and action_reference_new_table is NULL, the the script returns NULL:
CASE WHEN action_reference_new_table IS NOT NULL THEN
chr(10) || chr(9) || 'REFERENCING NEW TABLE AS ' || action_reference_new_table || chr(10) END ||
I don't understand why the NULL value for action_reference_new_table blows up my concatenation code and makes the entire result NULL.
Apart from help on this specific question, feel free to point out whatever I should do to write more sensible PL/PgSQL code. It's proving to be harder for me to master than I would have guessed.

Simply use
SELECT pg_get_triggerdef(oid)
FROM pg_trigger
WHERE tgname = trigger_name_in;
Besides, never use string concatenation when composing SQL code. The danger of SQL injection is too great. Use the format() function with the %I placeholder.

Related

Concat text variables in postgresql function

I have this code and I want to concatenate the variables but don't work.
This is my DDL code for the view:
CREATE OR REPLACE function acd.add_credito2()
RETURNS void
SET SCHEMA 'acd'
SET search_path = acd
AS $$
DECLARE
auxsigla text;
auxnome text;
_sql text := 'CREATE OR REPLACE VIEW acd.teste AS SELECT md.matriz_disciplina_id AS id, dcp.nome, mc.curso, mc.versao AS matriz';
_join text := ' FROM matriz_disciplina as md LEFT JOIN disciplina as dcp ON md.disciplina_id = dcp.disciplina_id LEFT JOIN matriz_curricular as mc ON md.matriz_curricular_id = mc.matriz_curricular_id';
BEGIN
select into auxsigla, auxnome from ( select sigla, nome from acd.categoria_credito where categoria_credito_id = 9) as foo;
_join := _join || ' LEFT JOIN (SELECT creditos, matriz_disciplina_id FROM acd.disciplina_credito WHERE categoria_credito_id = ' || x || ') AS ' || "auxsigla" ' ON ' || "auxsigla" || '.matriz_disciplina_id = md.matriz_disciplina_id';
_sql := _sql || ', ' || "auxsigla" || '.' || auxnome || ' AS ' || auxnome;
_sql := _sql || _join;
EXECUTE _sql;
END;
$$ LANGUAGE plpgsql
So, when I execute the function
database-1=# select acd.add_credito2();
This error appears:
ERROR: type "auxsigla" does not exist
LINE 1: ...WHERE categoria_credito_id = ' || x || ') AS ' || "auxsigla"...
^
QUERY: SELECT _join || ' LEFT JOIN (SELECT creditos, matriz_disciplina_id FROM acd.disciplina_credito WHERE categoria_credito_id = ' || x || ') AS ' || "auxsigla" ' ON ' || "auxsigla" || '.matriz_disciplina_id = md.matriz_disciplina_id'
CONTEXT: PL/pgSQL function add_credito2() line 13 at assignment
Can anyone help me? I don't know what to do now.
(I know, this study view don't have a purpose but this is the idea that I want to use in the real view)
The error comes from this construct:
"auxsigla" ' ON '
You forgot a concatenation operator || between these two tokens, and now the SQL parser interprets it as
data_type string_constant
which is a way to specify constants of a certain data type.
Working examples would be DATE '2018-09-20' or INTEGER '-20'.
Your function has numerous other problems, two of which I could spot:
select into auxsigla, auxnome from will always set the variables to NULL because you forgot to specify which columns you want to select.
you do not properly escape single quotes while composing your dynamic query string. What if auxsigla has the value with'quote?
Use format() or quote_literal() and quote_ident() for that.

Postgresql - Generating where not Exists condition dynamically for re-runnable insert script

I need to generate Insert script in postgres for all the tables in a database such that it can be run again without throwing any error.
The problem is, Only few tables have primary key while the rest have Unique index on different columns.
This is why I am not able to list out the columns on which unique index has been created.
The reason behind this is that the schema is automatically created through Magnolia.
Can anyone help me write the query which produces Insert statement including 'Where not Exists (Select 1 from table where column = value)' condition based on Primary Key/Unique columns?
You can use on conflict:
insert into t ( . . . )
values ( . . . )
on conflict do nothing;
This function returns Insert script for data and works well with tables on which primary constraint is not available.
I have modified the code that I found on another thread by adding the condition to it.
CREATE OR REPLACE FUNCTION public.generate_inserts(varSchema text, varTable text) RETURNS TABLE(resultado text) AS $$
DECLARE CODE TEXT;
BEGIN
CODE :=
(
SELECT
'SELECT ''INSERT INTO '
|| table_schema || '.'
|| table_name ||' ('
|| replace(replace(array_agg(column_name::text)::text,'{',''),'}','') || ') SELECT ''||'
|| replace(replace(replace(array_agg( 'quote_nullable(' || column_name::text || ')')::text,'{',''),'}',''),',',' || '','' || ')
|| ' || '' Where Not Exists (Select 1 From ' || table_name ||' Where 1 = 1 '
|| ''''
|| replace(replace(replace(replace(array_agg(' || '' and (' || column_name::text || ' = '' || quote_nullable(' || column_name::text || '),' || ' || '' or ' || column_name::text || ' is null)''')::text,'{',''),'}',''),'"',''),',','')
|| '|| '');'''
|| ' FROM ' || table_schema || '.' || table_name || ';'
FROM information_schema.columns c
WHERE table_schema = varSchema
AND table_name = varTable
GROUP BY table_schema, table_name);
RETURN QUERY
EXECUTE CODE;
END;
$$ LANGUAGE plpgsql;

Postgresql - full text search index - unexpected query results

I have a table with a bunch of cols
I have created a full text index on a table like this:
CREATE INDEX phrasetable_exp_idx ON msc.mytable
USING gin(to_tsvector('norwegian', coalesce(msc.mytable.col1,'') || ' ' ||
coalesce(msc.mytable.col2,'') || ' ' ||
coalesce(msc.mytable.col3,'') || ' ' ||
coalesce(msc.mytable.col4,'') || ' ' ||
coalesce(msc.mytable.col5,'') || ' ' ||
coalesce(msc.mytable.col6,'') || ' ' ||
coalesce(msc.mytable.col7,'')));
I try some searches and they are lightning fast, however, for one particular search I don't get the expected results.
I have a row in my table where both col1 and col2 have the exact value "Importkompetanse Oslo AS"
in col3 it has the value "9999".
Only the query to_tsquery('9999') returns the row, which shows me that it does have the value "Importkompetanse Oslo AS" in the both col1 and col2, but the first two queries return no matches.
SELECT *
FROM msc.mytable
WHERE to_tsvector('norwegian', coalesce(msc.col1,'') || ' ' ||
coalesce(msc.mytable.col2,'') || ' ' ||
coalesce(msc.mytable.col3,'') || ' ' ||
coalesce(msc.mytable.col4,'') || ' ' ||
coalesce(msc.mytable.col5,'') || ' ' ||
coalesce(msc.mytable.col6,'') || ' ' ||
coalesce(msc.mytable.col7,'')));
## --to_tsquery('Importkompetanse&Oslo&AS') -- nada
plainto_tsquery('Importkompetanse') -- nada
--to_tsquery('9999') -- OK!
Does anyone have an idea why my searches yields no results?
EDIT:
For some reason, to_tsquery returns something like this:
"'9999':9 'importkompetans':1,6"
The word importkompetanse seems to be cut off?
However, if I set it to simple instead of norwegian, I get the expected results and everything looks good. Why is that?
You used cross configuration between your tsvector and tsquery values. You should use consistent configuration, like:
select to_tsvector('norwegian', 'Importkompetanse Oslo AS')
## to_tsquery('norwegian', 'Importkompetanse&Oslo&AS');
SQLFiddle
This is why it worked with the 'simple' configuration (that is your default).
Note: you can always debug text search with ts_debug(): f.ex. 'Importkompetanse' has not been cut off, 'importkompetans' is just the appropriate lexeme for this word (in the 'norwegian' configuration).
Off: you use a really long, expression-based index, which will only be used, if you use the exact expression in your queries too. You used it right in your example, but this makes your queries really long, and if you change your index expression some time later, you need to make sure all "uses" updated as well.
You could use a simple (sql) function, to simplify your queries:
create or replace function col_tsvector(mytable)
returns tsvector
immutable
language sql
as $function$
return to_tsvector('norwegian',
coalesce($1.col1, '') || ' ' ||
coalesce($1.col2, '') || ' ' ||
coalesce($1.col3, '') || ' ' ||
coalesce($1.col4, '') || ' ' ||
coalesce($1.col5, '') || ' ' ||
coalesce($1.col6, '') || ' ' ||
coalesce($1.col7, ''))
$function$;
With this, you can greatly simplify your index definition & your queries too. (You can even use the attribute notation.)

PostgreSQL ERROR: EXECUTE of SELECT ... INTO is not implemented

When I run the following command from a function I defined, I get the error "EXECUTE of SELECT ... INTO is not implemented". Does this mean the specific command is not allowed (i.e. "SELECT ...INTO")? Or does it just mean I'm doing something wrong? The actual code causing the error is below. I apologize if the answer is already out here, however I looked and could not find this specific error. Thanks in advance... For whatever it's worth I'm running 8.4.7
vCommand = 'select ' || stmt.column_name || ' as id ' ||
', count(*) as nCount
INTO tmpResults
from ' || stmt.table_name || '
WHERE ' || stmt.column_name || ' IN (select distinct primary_id from anyTable
WHERE primary_id = ' || stmt.column_name || ')
group by ' || stmt.column_name || ';';
EXECUTE vCommand;
INTO is ambiguous in this use case and then is prohibited there.
You can use a CREATE TABLE AS SELECT instead.
CREATE OR REPLACE FUNCTION public.f1(tablename character varying)
RETURNS integer
LANGUAGE plpgsql
AS $function$
begin
execute 'create temp table xx on commit drop as select * from '
|| quote_ident(tablename);
return (select count(*) from xx);
end;
$function$
postgres=# select f1('omega');
f1
────
2
(1 row)

Inserting NEW.* from a generic trigger using EXECUTE in PL/pgsql

I have a number of tables that use the Postgres "Partitioning" feature. I want to define a common BEFORE INSERT OF ROW trigger on each table that will 1) dynamically create the partition should the insert occur against the parent table and 2) re-execute the insert against the partition.
Something like:
CREATE OR REPLACE FUNCTION partition_insert_redirect( )
RETURNS trigger AS $BODY$
BEGIN
... create the new partition and set up the redirect Rules ...
/* Redo the INSERT dynamically. The new RULE will redirect it to the child table */
EXECUTE 'INSERT INTO ' || quote_ident(TG_TABLE_SCHEMA) || '.' || quote_ident(TG_TABLE_NAME) ||
' SELECT NEW.*'
END
But the "NEW" record isn't visible inside the EXECUTE SQL. How can I make this work as simply as possible?
As an alternative, can I iterate over the fields in the NEW record somehow?
I've thought of using a temp-table:
EXECUTE 'CREATE TEMPORARY TABLE new_row (LIKE ' ||
quote_ident(TG_TABLE_SCHEMA) || '.' || quote_ident(TG_TABLE_NAME) ||
') ON COMMIT DROP';
INSERT INTO new_row SELECT NEW.*;
EXECUTE 'INSERT INTO ' || quote_ident(TG_TABLE_SCHEMA) || '.' || quote_ident(TG_TABLE_NAME) ||
' SELECT * FROM new_row';
DROP TABLE new_row;
But this also doesn't work because of the cached reference to a temp-table: Why do I get "relation with OID ##### does not exist" errors when accessing temporary tables in PL/PgSQL functions?
I'm using Postgres 8.2 and I can't change to any other version.
EDIT:
As #alvherre pointed out, this can probably be done in Postgres 8.4 with the EXECUTE ... USING syntax. See an example at http://wiki.postgresql.org/wiki/PL/pgSQL_Dynamic_Triggers
You can use EXECUTE USING to pass NEW to it. Your example would be
EXECUTE 'INSERT INTO ' || TG_RELID || '::regclass SELECT $1' USING NEW;
(Note that I use TG_RELID casted to regclass instead of fiddling with TG_TABLE_SCHEMA and TABLE_NAME because it is easier to use, if nonstandard. But then, plpgsql is nonstandard anyway.)
Yes, you can use EXECUTE ... USING in 8.4. For example:
EXECUTE 'INSERT INTO ' || table_name || ' SELECT $1.*' USING NEW;
In lower versions (I've only tested in 8.3), you can use:
EXECUTE 'INSERT INTO ' || table_name ||
' SELECT (' || quote_literal(NEW) || '::' || TG_RELID::regclass || ').*';
I've managed to get this to work by dynamically compiling a function that accepts the NEW row as a parameter:
EXECUTE 'create or replace function partition_insert(r ' || TG_TABLE_NAME || ') RETURNS void AS $FUNC$' ||
'BEGIN ' ||
'insert into ' || TG_TABLE_NAME || ' SELECT r.*; ' ||
'END $FUNC$ LANGUAGE plpgsql VOLATILE';
PERFORM partition_insert(NEW);
As Postgres functions are polymorphic, this will generate a different function for each table that uses this trigger.
Despite being an ugly kludge, this seems to do the job.
Although it looks like I could define each polymorphic variation up front when I build the system, because of caching, I must recompile the function whenever I create or drop a child table so that the function uses the latest insert RULE.
EDIT: Additional wrinkles
There's a little gotcha with this technique: If this EXECUTE/PERFORM action is rolled-back on the first attempt due to another error (for example, in my case a CHECK constraint failure) then the function containing this code seems to cache a reference to the rolled-back partition_insert() function it created using the EXECUTE and subsequent calls fail due to a cached object not being found.
I resolved this by pre-creating stub versions of the function for each required table-type parameter when I define the database.