PostgreSQL trigger to generate codes for multiple tables dynamically - postgresql

I'd like to generate codes for many tables in the database, and stopped to refactor my solution when I was ready to write my third implementation of "get code for table X".
My code is this:
-- Tenants receive a code that's composed of a portion of their subdomain and a unique number.
-- This number comes from this sequence.
CREATE SEQUENCE tenant_codes_seq MAXVALUE 9999 NO CYCLE;
CREATE TABLE tenants (
subdomain varchar(36) NOT NULL UNIQUE
, tenant_code char(8) NOT NULL UNIQUE
, PRIMARY KEY (tenant_code)
);
-- This function expects four parameters:
-- 1. The column that's receiving the generated code (RECEIVING_COLUMN_NAME)
-- 2. The column that's used to salt the code (SALT_COLUMN_NAME)
-- 3. The number of characters to use from the salt column (SALT_LENGTH)
-- 4. The sequence name, but defaults to RECEIVING_COLUMN_NAME || 's'
CREATE OR REPLACE FUNCTION generate_table_code() RETURNS trigger AS $$
DECLARE
receiving_column_name text;
salt_column_name text;
salt_length text;
sequence_name text;
BEGIN
receiving_column_name := TG_ARGV[0];
salt_column_name := TG_ARGV[1];
salt_length := TG_ARGV[2];
CASE
WHEN TG_NARGS = 3 THEN
sequence_name := receiving_column_name || 's';
WHEN TG_NARGS = 4 THEN
sequence_name := TG_ARGV[3];
ELSE
RAISE EXCEPTION '3 or 4 arguments expected, received %', TG_NARGS;
END CASE;
-- The intent is to return ABC-0001 when salt_column contains 'ABC'
EXECUTE 'rpad(substr(' ||
quote_ident(salt_column_name) ||
', 1, 4), 4, ' ||
quote_literal('-') ||
') || lpad(nextval(' ||
quote_literal(sequence_name) ||
')::text, ' ||
quote_literal(salt_length) ||
', ' ||
quote_literal('0') ||
')'
INTO STRICT NEW;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER generate_tenant_code_trig
BEFORE INSERT ON tenants FOR EACH ROW
EXECUTE PROCEDURE generate_table_code('tenant_code', 'subdomain', 4);
How do I assign to NEW.tenant_code, NEW.user_code or NEW.table_whatever_code?
Running some tests yields the correct "statement", but I can't seem to assign correctly:
INSERT INTO tenants(subdomain) VALUES ('abc')
CREATE TABLE
ERROR: syntax error at or near "NEW"
LINE 1: NEW.tenant_code := rpad(substr(subdomain, 1, 4), 4, '-') || ...
^
QUERY: NEW.tenant_code := rpad(substr(subdomain, 1, 4), 4, '-') || lpad(nextval('tenant_codes')::text, '4', '0'::text)
CONTEXT: PL/pgSQL function "generate_table_code" line 20 at EXECUTE statement

I'd be quite enthusiastic to be shown wrong (I occasionally need this myself too), but best I'm aware, referring column names using variables is one of those cases where you actually need to use PL/C triggers rather than PL/PgSQL triggers. You'll find examples of such triggers in contrib/spi and on PGXN.
Alternatively, name your columns consistently so as to be able to reference them directly, e.g. NEW.tenant_code.
Personally, I generally end up writing a function that creates the trigger:
create function create_tg_stuff(_table regclass, _args[] text[])
returns void as $$
begin
-- explore pg_catalog a bit
execute $x$
create function $x$ || quote_ident(_table || '_tg_stuff') || $x$()
returns trigger as $t$
begin
-- more stuff
return new;
end;
$t$ language plpgsql;
$x$;
end;
$$ language plpgsql;

NEW is type RECORD, so you can't assign to that AFAIK.
To set the value of a column, assign to NEW.column, for example:
NEW.tenant_code := (SELECT some_calculation);
Maybe your design is too complicated; PL/SQL is a very limited language - try to make your code as simple as possible

Related

PostgreSQL plpgsql - variable column names

I am creating a trigger, which uses dynamic names for columns
NEW.name:=2222; -- works fine !
but
dynamic_column:='name';
EXECUTE '$1.'||dynamic_column||':=2222 ' USING NEW; -- raises error
gives an error:
ERROR: syntax error at or near "$1" LINE 1: $1.name:=2222
I found info here: Assign to NEW by key in a Postgres trigger
If we enable the module hstore by:
CREATE EXTENSION hstore;
We can do this:
dynamic_column:='name';
temp_sql_string:='"'||dynamic_column||'"=>"2222"';
NEW := NEW #= temp_sql_string::hstore;
And the RECORD NEW.name now is set to the value 2222.
Thank you tough for making an effort to find a solution #Laurenz Albe
The problem is that this is not a valid SQL statement.
You can access the columns in new with dynamic SQL like this:
EXECUTE 'SELECT $1.id' INTO v_id USING NEW;
There is no comfortable way like that for changing individual columns in NEW.
You could use TG_RELID to get the OID of the table, query pg_attribute for the columns, compose a row literal string composed of the values in NEW and your new value, cast this to the table type and assign the result to NEW. Quite cumbersome.
Here is sample code that does that (I tested it, but there may be bugs left):
CREATE OR REPLACE FUNCTION dyntrig() RETURNS trigger
LANGUAGE plpgsql AS
$$DECLARE
colname text;
colval text;
newrow text := '';
fieldsep text := 'ROW(';
BEGIN
/* loop through the columns of the table */
FOR colname IN
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = TG_RELID
AND attnum > 0
AND NOT attisdropped
ORDER BY attnum
LOOP
IF colname = 'name' THEN
colval = '2222';
ELSE
/* all other columns than 'name' retain their value */
EXECUTE 'SELECT CAST($1.' || quote_ident(colname) || ' AS text)'
INTO colval USING NEW;
END IF;
/* compose a string that represents the new table row */
IF colval IS NULL THEN
newrow := newrow || fieldsep || 'NULL';
ELSE
newrow := newrow || fieldsep || '''' || colval || '''';
END IF;
fieldsep := ',';
END LOOP;
newrow := newrow || ')';
/* assign the new table row to NEW */
EXECUTE 'SELECT (CAST(' || newrow || ' AS '
|| quote_ident(TG_TABLE_SCHEMA) || '.' || quote_ident(TG_TABLE_NAME)
|| ')).*'
INTO NEW;
RETURN NEW;
END;$$;
You already found my answer recommending the hstore operator #= on dba.SE. You may also be interested in the corresponding reference answer here on SO:
How to set value of composite variable field using dynamic SQL
Since you construct the auxiliary hstore value from variables I suggest the simple function hstore():
CREATE OR REPLACE FUNCTION dyn_trigger_func()
RETURNS TRIGGER AS
$func$
DECLARE
dyn_col_name text := 'name';
dyn_col_val text := '2222';
BEGIN
NEW := NEW #= hstore(dyn_col_name, dyn_col_val);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Faster / simpler / clearer / more secure this way.
Or, since it's obviously a trigger function, you may want to pass column name and value in CREATE TRIGGER statements:
CREATE OR REPLACE FUNCTION dyn_trigger_func()
RETURNS TRIGGER AS
$func$
BEGIN
NEW := NEW #= hstore(TG_ARGV[0], TG_ARGV[1]);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
And:
CREATE TRIGGER ins_bef
BEFORE INSERT ON tbl
FOR EACH ROW EXECUTE PROCEDURE dyn_trigger_func('name', '2222');
Provide column name unquoted and case-sensitive.
Related:
Get values from varying columns in a generic trigger
Trigger with dynamic field name

Trigger with dynamic field name

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:

Creating a trigger for child table insertion returns confusing error

I am trying to write a trigger function that will input values into separate child tables, however I am getting an error I have not seen before.
Here is an example set up:
-- create initial table
CREATE TABLE public.testlog(
id serial not null,
col1 integer,
col2 integer,
col3 integer,
name text
);
-- create child table
CREATE TABLE public.testlog_a (primary key(id)) INHERITS(public.testlog);
-- make trigger function for insert
CREATE OR REPLACE FUNCTION public.test_log() RETURNS trigger AS
$$
DECLARE
qry text;
BEGIN
qry := 'INSERT INTO public.testlog_' || NEW.name || ' SELECT ($1).*';
EXECUTE qry USING NEW.*;
RETURN OLD;
END
$$
LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
-- add function to table
CREATE TRIGGER test_log_sorter BEFORE INSERT
ON public.testlog FOR EACH ROW
EXECUTE PROCEDURE public.test_log();
and the query:
INSERT INTO public.testlog (col1, col2, col3, name) values (1, 2, 3, 'a');
error message:
[Err] ERROR: query "SELECT NEW.*" returned 5 columns
CONTEXT: PL/pgSQL function test_log() line 7 at EXECUTE statement
5 columns is exactly what I am looking for it to return, so clearly there is something I am not understanding but the error message seems to make no sense.
Can anybody explain why I am getting this?
Your solution fixes the passing of the row-typed NEW variable. However, you have a sneaky SQL-injection hole in your code, that's particularly dangerous in a SECURITY DEFINER function. User input must never be converted to SQL code unescaped.
Sanitize like this:
CREATE OR REPLACE FUNCTION trg_test_log()
RETURNS trigger AS
$$
BEGIN
EXECUTE 'INSERT INTO public.' || quote_ident('testlog_' || NEW.name)
|| ' SELECT ($1).*'
USING NEW;
RETURN NULL;
END
$$
LANGUAGE plpgsql SECURITY DEFINER;
Also:
OLD is not defined in an INSERT trigger.
You don't need a variable. Assignments are comparatively expensive in plpgsql.
The EXECUTE qry USING NEW.* passes in the NEW.* as the arguments to the query. Since NEW.* returns five columns, the query should have $1, $2, $3, $4 and $5 in order to bind the five columns.
You are expecting a single argument ($1) which has five columns in it. I believe that if you change the the line to
EXECUTE qry USING NEW;
it will work as you expect.
With regards to Robert M. Lefkowitz' response, the answer is so simple: NEW as opposed to NEW.*
CREATE OR REPLACE FUNCTION public.test_log() RETURNS trigger AS
$$
DECLARE
qry text;
BEGIN
qry := 'INSERT INTO public.testlog_' || NEW.name || ' SELECT ($1).*';
EXECUTE qry USING NEW;
RETURN OLD;
END
$$
LANGUAGE plpgsql VOLATILE SECURITY DEFINER
COST 100;
thanks.

Dynamic SQL in Postgres

I've coded a simple Function using Postgres but keep getting the following:
ERROR: syntax error at or near "$2".
The underlying database is ParAccel and I'm new to both Postgres and ParAccel. I'm using TOAD Data Point as the IDE:
CREATE OR REPLACE FUNCTION GET_NEXT_SURR_KEY(I_SCHEMA_NM VARCHAR, I_TABLE_NM VARCHAR,I_COLUMN_NM VARCHAR,I_POSNEG_FLAG VARCHAR)
RETURNS BIGINT
LANGUAGE PLPGSQL
AS $body$
DECLARE
O_RET_VALUE BIGINT := 0;
V_DYN_SQL VARCHAR(2000) := '';
BEGIN
IF I_POSNEG_FLAG = 'P' THEN
V_DYN_SQL := 'SELECT MAX(' || I_COLUMN_NM || ') + 1 FROM ' || I_SCHEMA_NM || '.' || I_TABLE_NM;
ELSE
V_DYN_SQL := 'SELECT MIN(' || I_COLUMN_NM || ') - 1 FROM ' || I_SCHEMA_NM || '.' || I_TABLE_NM;
END IF;
EXECUTE V_DYN_SQL INTO O_RET_VALUE;
RETURN O_RET_VALUE;
END $body$
I'm using the following example command to execute the Function:
{CALL GET_NEXT_SURR_KEY('some_schema_name','some_table_name','some_column_name','P')};
Can anyone please let me know where I'm messing up?
Thanks in advance.
ParAccel has the concept of IDENTITY fields, not sure why you are not using them.
But in any case, here is how to solve your problem.
BTW, I believe the code you wrote would work on PostgreSQL 9 or above, but ParAccel is using version 7.02 (If I'm not mistaken) which doesn't support SELECT INTO a variable so you need to capture the result with a record and extract the value using a loop (I didn't re-wrote all your function, just the main part)
CREATE OR REPLACE FUNCTION GET_NEXT_SURR_KEY(I_SCHEMA_NM VARCHAR, I_TABLE_NM VARCHAR,I_COLUMN_NM VARCHAR,I_POSNEG_FLAG VARCHAR)
RETURNS BIGINT
LANGUAGE PLPGSQL
AS $body$
DECLARE
O_RET_VALUE BIGINT default 0;
V_DYN_SQL VARCHAR(2000) := '';
_ret_rec record;
BEGIN
V_DYN_SQL := 'SELECT MAX(' || I_COLUMN_NM || ') + 1 as new_id FROM ' || I_SCHEMA_NM || '.' || I_TABLE_NM;
FOR _ret_rec IN EXECUTE V_DYN_SQL
LOOP
O_RET_VALUE := _ret_rec.new_id;
END LOOP;
RETURN O_RET_VALUE;
END $body$
Trust the horse, use sequences - because you are in an OLAP environment you most likely will not get uniqueness violations but if this would be a normal website you would get the same id twice very often. As for your function it works perfectly well - tested it on a random table in my database and no error was given so look for the fault in TOAD.

EXECUTE...USING statement in PL/pgSQL doesn't work with record type?

I'm trying to write a function in PL/PgSQL that have to work with a table it receives as a parameter.
I use EXECUTE..INTO..USING statements within the function definition to build dynamic queries (it's the only way I know to do this) but ... I encountered a problem with RECORD data types.
Let's consider the follow (extremely simplified) example.
-- A table with some values.
DROP TABLE IF EXISTS table1;
CREATE TABLE table1 (
code INT,
descr TEXT
);
INSERT INTO table1 VALUES ('1','a');
INSERT INTO table1 VALUES ('2','b');
-- The function code.
DROP FUNCTION IF EXISTS foo (TEXT);
CREATE FUNCTION foo (tbl_name TEXT) RETURNS VOID AS $$
DECLARE
r RECORD;
d TEXT;
BEGIN
FOR r IN
EXECUTE 'SELECT * FROM ' || tbl_name
LOOP
--SELECT r.descr INTO d; --IT WORK
EXECUTE 'SELECT ($1)' || '.descr' INTO d USING r; --IT DOES NOT WORK
RAISE NOTICE '%', d;
END LOOP;
END;
$$ LANGUAGE plpgsql STRICT;
-- Call foo function on table1
SELECT foo('table1');
It output the following error:
ERROR: could not identify column "descr" in record data type
although the syntax I used seems valid to me. I can't use the static select (commented in the example) because I want to dinamically refer the columns names.
So..someone know what's wrong with the above code?
It's true. You cannot to use type record outside PL/pgSQL space.
RECORD value is valid only in plpgsql.
you can do
EXECUTE 'SELECT $1.descr' INTO d USING r::text::xx;
$1 should be inside the || ,like || $1 || and give spaces properly then it will work.
BEGIN
EXECUTE ' delete from ' || quote_ident($1) || ' where condition ';
END;