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.
Related
Is it possible to execute a dynamically created query in postgres without using a function?
The following query returns the queries that I want to execute:
SELECT 'ALTER INDEX ' || idx_name_1 || ' RENAME TO ' || idx_name_2 || ';' AS myqueries from myindexes;
This returns the following:
myqueries
------------
ALTER INDEX idxold1 RENAME TO idxnew1;
ALTER INDEX idxold2 RENAME TO idxnew2;
Now ideally I'd like to directly execute the ALTER commands within one psql command without having to use a function.
After the hint by a_horse_with_no_name, I got it to work with the following code:
DO $$DECLARE r record;
BEGIN
FOR r in SELECT idx_name_1, idx_name_2 from myindexes
LOOP
EXECUTE 'ALTER INDEX ' || quote_ident(r.idx_name_1) || ' RENAME TO ' || quote_ident(r.idx_name_2);
END LOOP;
END$$;
I'm struggling with a stored procedure which heavily uses dynamic queries. Among others I need to store maximum value of an existing column into a variable.
Postgres documents state "if you want to use dynamically determined table or column names, you must insert them into the command string textually". Based on that I've come up with following statement:
EXECUTE 'SELECT MAX(' || pkColumn::regclass || ') FROM ' ||
tableName::regclass INTO maxValue;
Table name seems to be OK, column name triggers error.
What am I doing wrong ?
Pavel
EXECUTE 'SELECT MAX(' || pkColumn ||'::regclass) FROM ' || ...
::regclass is a cast done inside query. You can also skip it, or put " - which in PG works the same. So please try one of:
EXECUTE 'SELECT MAX(' || pkColumn || ') FROM ' || ...
or
EXECUTE 'SELECT MAX("' || pkColumn || '") FROM ' || ...
All tree should work. If not - just let me know. In that case it is my fault, postgresql simply works.
There is no reason to cast parameters as they are just identifiers. For better control and readability use the function format(), e.g.:
declare
pkcolumn text = 'my_column';
tablename text = 'my_table';
...
execute format('select max(%I) from %I', pkcolumn, tablename)
into maxvalue;
I am trying to execute the following dynamic sql, but I could not figure out how to do it:
DROP FUNCTION f_mycross(text, text);
EXECUTE ('CREATE OR REPLACE FUNCTION f_mycross(text, text)
RETURNS TABLE ("registration_id" integer, '
|| (SELECT string_agg(DISTINCT pivot_headers, ',' order by pivot_headers)
FROM (SELECT DISTINCT '"' || qid::text || '" text' AS pivot_headers
FROM answers) x)
|| ') AS ''$libdir/tablefunc'',''crosstab_hash'' LANGUAGE C STABLE STRICT;')
I am relatively new to PostgreSQL.
Like a_horse commented, EXECUTE is not an SQL command. It's a PL/pgSQL command and can only be used in a function body or DO statement using this procedural language. Like:
DROP FUNCTION IF EXISTS f_mycross(text, text);
DO
$do$
BEGIN
EXECUTE (
SELECT 'CREATE OR REPLACE FUNCTION f_mycross(text, text)
RETURNS TABLE (registration_id integer, '
|| string_agg(pivot_header || ' text', ', ')
|| $$) AS '$libdir/tablefunc', 'crosstab_hash' LANGUAGE C STABLE STRICT$$
FROM (SELECT DISTINCT quote_ident(qid::text) AS pivot_header FROM answers ORDER BY 1) x
);
END
$do$; -- LANGUAGE plpgsql is the default
I added some improvements and simplified the nested SELECT query.
Major points
Add IF EXISTS to DROP FUNCTION unless you are certain the function exists or you want to raise an exception if it does not.
DISTINCT in the subquery is enough, no need for another DISTINCT in the outer SELECT.
Use quote_ident() to automatically double-quote identifiers where necessary.
No parentheses required around the string we feed to EXECUTE.
Simpler nested quoting with $-quotes.
Insert text with single quotes in PostgreSQL
We can apply ORDER BY in the subquery, which is typically much faster than adding ORDER BY in the outer aggregate function.
I have a problem on creating PostgreSQL (9.3) trigger on update table.
I want set new values in the loop as
EXECUTE 'NEW.'|| fieldName || ':=''some prepend data'' || NEW.' || fieldName || ';';
where fieldName is set dynamically. But this string raise error
ERROR: syntax error at or near "NEW"
How do I go about achieving that?
You can implement that rather conveniently with the hstore operator #=:
Make sure the additional module is installed properly (once per database), in a schema that's included in your search_path:
How to use % operator from the extension pg_trgm?
Best way to install hstore on multiple schemas in a Postgres database?
Trigger function:
CREATE OR REPLACE FUNCTION tbl_insup_bef()
RETURNS TRIGGER AS
$func$
DECLARE
_prefix CONSTANT text := 'some prepend data'; -- your prefix here
_prelen CONSTANT int := 17; -- length of above string (optional optimization)
_col text := quote_ident(TG_ARGV[0]);
_val text;
BEGIN
EXECUTE 'SELECT $1.' || _col
USING NEW
INTO _val;
IF left(_val, _prelen) = _prefix THEN
-- do nothing: prefix already there!
ELSE
NEW := NEW #= hstore(_col, _prefix || _val);
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Trigger (reuse the same func for multiple tables):
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE ON tbl
FOR EACH ROW
EXECUTE PROCEDURE tbl_insup_bef('fieldName'); -- unquoted, case-sensitive column name
Closely related with more explanation and advice:
Assignment of a column with dynamic column name
How to access NEW or OLD field given only the field's name?
Get values from varying columns in a generic trigger
Your problem is that EXECUTE can only be used to execute SQL statements and not PL/pgSQL statements like the assignment in your question.
You can maybe work around that like this:
Let's assume that table testtab is defined like this:
CREATE TABLE testtab (
id integer primary key,
val text
);
Then a trigger function like the following will work:
BEGIN
EXECUTE 'SELECT $1.id, ''prefix '' || $1.val' INTO NEW USING NEW;
RETURN NEW;
END;
I used hard-coded idand val in my example, but that is not necessary.
I found a working solution:
trigger should execute after insert/update, not before. Then desired row takes the form
EXECUTE 'UPDATE ' || TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME ||
' SET ' || fieldName || '= ''prefix:'' ||''' || fieldValue || ''' WHERE id = ' || NEW.id;
fieldName and fieldValue I get in the next way:
FOR fieldName,fieldValue IN select key,value from each(hstore(NEW)) LOOP
IF .... THEN
END LOOP:
I want to execute the following:
EXECUTE 'SELECT ' || row_name || ' INTO row_value FROM user_data.data WHERE id = ' || tid || ';';
row_name, row_value and tid are variables of the plpgsql function. My concern is, whether the selected value will be passed to the row_value variable or not.
Try it yourself! Your function raises
ERROR: EXECUTE of SELECT ... INTO is not implemented
HINT: You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.
and all is clear.