Getting row values based on array of column names passed to PostgreSQL function - postgresql

I'm trying to set up full text search in PostgreSQL 9.2. I created a new table to hold the content that I want to search (so that I can search across lots of different types of items), which looks like this:
CREATE TABLE search (
target_id bigint PRIMARY KEY,
target_type text,
fts tsvector
);
CREATE INDEX search_fts ON search USING gin(fts);
Every time a new item gets inserted (or updated) into one of the various tables I want to search across, it should automatically be added to the search table. Assuming that my table looks like the following:
CREATE TABLE item (id bigint PRIMARY KEY, name text NOT NULL, description text);
I created a trigger passing in the column names that I want to be able to search:
CREATE TRIGGER insert_item_search BEFORE INSERT
ON item FOR EACH ROW EXECUTE PROCEDURE
insert_search('{name, description}'::text[]);
Then created a new function insert_search as:
CREATE OR REPLACE FUNCTION insert_search(cols text[]) RETURNS TRIGGER AS $$
BEGIN
INSERT INTO search (target_id, target_type, fts) VALUES (
NEW.id, TG_TABLE_NAME, to_tsvector('english', 'foo')
);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
My question is, how do I pass in the table values based on cols to to_tsvector? Right now, the function is getting called and inserts the id and type correctly, but I don't know the right way to dynamically grab the other values based on the cols argument.

First, to pass arguments, just send them directly:
CREATE TRIGGER insert_item_search BEFORE INSERT
ON item FOR EACH ROW EXECUTE PROCEDURE
insert_search('name', 'description');
And, from PL/pgSQL you will get those arguments as an array, called TG_ARGV. But, the problem is that PL/pgSQL cannot get the values from NEW record based on their names. To do that you can either use a language that lets you do that (like PL/python or PL/perl) or use the hstore extension.
I'd stick with the last one and use hstore (unless you already use one of the other languages to create functions):
CREATE OR REPLACE FUNCTION insert_search() RETURNS TRIGGER AS $$
DECLARE
v_new hstore;
BEGIN
v_new = hstore(NEW); -- convert the record to hstore
FOR i IN 0..(TG_NARGS-1) LOOP
INSERT INTO search (target_id, target_type, fts) VALUES (
NEW.id, TG_TABLE_NAME, to_tsvector('english', v_new -> TG_ARGV[i])
);
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
As you can see above, I used the hstore's operator -> to get the value based on the name (on TG_ARGV[i]).

You can access the parameters specified in the trigger definition with the TG_ARGV variable. You can find documentation on that here. TG_ARGV is an array accessed by a 0 based index. So it would be something like TG_ARGV[0], TG_ARGV[1], and so on.

Related

Error when creating a generated column in Postgresql

CREATE TABLE Person (
id serial primary key,
accNum text UNIQUE GENERATED ALWAYS AS (
concat(right(cast(extract year from current_date) as text), 2), cast(id as text)) STORED
);
Error: generation expression is not immutable
The goal is to populate the accNum field with YYid where YY is the last two letters of the year when the person was added.
I also tried the '||' operator but it was unsuccessful.
As you don't expect the column to be updated, when the row is changed, you can define your own function that generates the number:
create function generate_acc_num(id int)
returns text
as
$$
select to_char(current_date, 'YY')||id::text;
$$
language sql
immutable; --<< this is lying to Postgres!
Note that you should never use this function for any other purpose. Especially not as an index expression.
Then you can use that in a generated column:
CREATE TABLE Person
(
id integer generated always as identity primary key,
acc_num text UNIQUE GENERATED ALWAYS AS (generate_acc_num(id)) STORED
);
As #ScottNeville correctly mentioned:
CURRENT_DATE is not immutable. So it cannot be used int a GENERATED ALWAYS AS expression.
However, you can achieve this using a trigger nevertheless:
demo:db<>fiddle
CREATE FUNCTION accnum_trigger_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
NEW.accNum := right(extract(year from current_date)::text, 2) || NEW.id::text;
RETURN NEW;
END
$$;
CREATE TRIGGER tr_accnum
BEFORE INSERT
ON person
FOR EACH ROW
EXECUTE PROCEDURE accnum_trigger_function();
As #a_horse_with_no_name mentioned correctly in the comments: You can simplify the expression to:
NEW.accNum := to_char(current_date, 'YY') || NEW.id;
I am not exactly sure how to solve this problem (maybe a trigger), but current_date is a stable function not an immutable one. For the generated IDs I believe all function calls must be immutable. You can read more here https://www.postgresql.org/docs/current/xfunc-volatility.html
I dont think any function that gets the date can be immutable as Postgres defines this as "An IMMUTABLE function cannot modify the database and is guaranteed to return the same results given the same arguments forever." This will not be true for anything that returns the current date.
I think your best bet would be to do this with a trigger so on insert it sets the value.

Postgres Trigger Function Dynamic Column Concat TG_TABLE_NAME

I want to know if it's possible to do something like this on Postgres 11 in a trigger function.
CREATE OR REPLACE FUNCTION "searchs"."handle_content" ( )
RETURNS trigger
AS $BODY$
BEGIN
UPDATE table
set test = 1
WHERE item_id = NEW.format('%I_id',TG_TABLE_NAME);
END;
$BODY$
The primary key on table change, so i need to concat with the table name
NEW.format('%I_id',TG_TABLE_NAME)
I have solved with CASE, but I still wish to know if it's possible to convert to an array record type, or access it dynamic.

PLPGSQL function returning trigger AND value

I have written a PL/PGSQL function that returns a trigger, so I can call it before each row insert. I realize now that I would also like that function to return the ID of the newly inserted row. I'm not quite sure how to proceed since my function must return a trigger. Here's some code:
CREATE OR REPLACE FUNCTION f_insert_album() RETURNS TRIGGER AS $$
DECLARE
subj_album_id INTEGER;
BEGIN
-- ... some parts where left out
INSERT INTO t_albums_subjective (user_id, album_id, format_id, location_id, rating)
VALUES (NEW.user_id, obj_album_id, NEW.format_id, NEW.location_id, NEW.rating)
RETURNING id INTO subj_album_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Bind insert function to trigger
DROP TRIGGER IF EXISTS tr_v_albums_insert ON v_albums;
CREATE TRIGGER tr_v_albums_insert INSTEAD OF INSERT ON v_albums
FOR EACH ROW EXECUTE PROCEDURE f_insert_album();
I must keep the return type of my function f_insert_album() to TRIGGER, but I would really like to also return the value in subj_album_id, corresponding to the id of the newly inserted row.
Is there anyway to do this? Is it even possible? Obviously changing the return type didn't work with Postgres. Could you suggest an alternative approach if any?
The crucial question: where to return the ID to?
Assuming you want to return it from the INSERT statement directly, then you are almost there. You already assign the newly generated ID to a function parameter:
...
RETURNING id INTO subj_album_id;
Instead, assign it to a column of the row firing the trigger. The special variable NEW holds this row in a trigger function:
...
RETURNING id INTO NEW.album_id; -- use actual column name in view
Then use the RETURNING clause of the INSERT statement:
INSERT INTO v_albums (user_id, format_id, location_id, rating)
VALUES ( ... )
RETURNING album_id;
Obviously, this is only possible if there is a visible column in the view. It does not have to be assigned in the INSERT command, though. The type of the NEW row variable is defined by the definition of the view, not by the INSERT at hand.
Closely related:
RETURNING data from updatable view not working?
For what you seem to be doing (grant access to certain rows of a table to a certain role) row level security (RLS) in Postgres 9.5 or later might be a more convenient alternative:
CREATE POLICY in the manual
The Postgres Wiki: "What's new in PostgreSQL 9.5"
Review by Depesz

Get values from varying columns in a generic trigger

I am new to PostgreSQL and found a trigger which serves my purpose completely except for one little thing. The trigger is quite generic and runs across different tables and logs different field changes. I found here.
What I now need to do is test for a specific field which changes as the tables change on which the trigger fires. I thought of using substr as all the column will have the same name format e.g. XXX_cust_no but the XXX can change to 2 or 4 characters. I need to log the value in theXXX_cust_no field with every record that is written to the history_ / audit table. Using a bunch of IF / ELSE statements to accomplish this is not something I would like to do.
The trigger as it now works logs the table_name, column_name, old_value, new_value. I however need to log the XXX_cust_no of the record that was changed as well.
Basically you need dynamic SQL for dynamic column names. format helps to format the DML command. Pass values from NEW and OLD with the USING clause.
Given these tables:
CREATE TABLE tbl (
t_id serial PRIMARY KEY
,abc_cust_no text
);
CREATE TABLE log (
id int
,table_name text
,column_name text
,old_value text
,new_value text
);
It could work like this:
CREATE OR REPLACE FUNCTION trg_demo()
RETURNS TRIGGER AS
$func$
BEGIN
EXECUTE format('
INSERT INTO log(id, table_name, column_name, old_value, new_value)
SELECT ($2).t_id
, $3
, $4
,($1).%1$I
,($2).%1$I', TG_ARGV[0])
USING OLD, NEW, TG_RELNAME, TG_ARGV[0];
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER demo
BEFORE UPDATE ON tbl
FOR EACH ROW EXECUTE PROCEDURE trg_demo('abc_cust_no'); -- col name here.
SQL Fiddle.
Related answer on dba.SE:
How to access NEW or OLD field given only the field's name?
List of special variables visible in plpgsql trigger functions in the manual.

How to create a Trigger in PostgreSql?

TRIGEER-->To get a column value from one table to other table when i insert values?
I am having two tables(customer_details and loan_balance).
What i need is, I must get the column (custid)of customer_details table to the loan_balance table when i insert the data into the loan_balance table.
This is the full set up of my query : SQL FIDDLE
So i need a trigger to be raised and the data should be updated automatically without dynamic insertion of custid.
Postgres has an unconventional way of creating triggers:
create a function that returns type trigger and return the NEW row record
create a trigger that executes the function
Here's the code you need:
CREATE FUNCTION synch_custid_proc()
RETURNS trigger AS $$
BEGIN
NEW.custid = (
select max(custid)
from customer_details
where creditid = NEW.creditid
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql
CREATE TRIGGER synch_custid_trig
BEFORE INSERT ON loan_amount
FOR EACH ROW
EXECUTE PROCEDURE synch_custid_proc();
I chosen to select max(custid) rather than simply custid when finding the value in case there are multiple rows that match. You might have to adjust this logic to suit your data.
See a live demo on SQLFiddle