In Postgres, someone knows how to substitute the value of the variable in a NEW.variable in a trigger?
For instance, I have a variable with value order_code. I want to execute NEW.variable so that it's getting in fact NEW.order_code.
In detailed:
I have a function to obtain the primary key column of a table:
CREATE FUNCTION getPrimaryKey(_table_name VARCHAR(50))
RETURNS SETOF VARCHAR(50) AS $$
DECLARE
primary_key VARCHAR(50);
BEGIN
FOR primary_key IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = _table_name::regclass
AND i.indisprimary LOOP
RETURN NEXT primary_key;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Then I have a trigger to collect some info when an INSERT is done in a table. The procedure in the trigger is called from several triggers from different tables. That's why it's so generic and I have this need.
What I want is to obtain the primary key of the object inserted.
CREATE FUNCTION logAudit()
RETURNS trigger AS $$
DECLARE primary_key VARCHAR(50);
BEGIN
primary_key := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
INSERT INTO test VALUES (TG_TABLE_NAME);
INSERT INTO test VALUES (NEW.primary_key);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_in_client
AFTER INSERT ON tb_client
FOR EACH STATEMENT EXECUTE PROCEDURE logAudit();
The NEW.primary_key is what is causing me issues. I expect primary_key to be the column name of the source table where the insert happened. What I want in NEW.primary_key is to actually use the value in the variable.
Here is the example of anonymous pl/pgsql block which doing something what you want:
do $$
declare
v pg_database = (pg_database) from pg_database where datname = 'template1';
fname text = 'datname';
n text;
begin
n := to_jsonb(v)->>fname;
raise info '%', n;
end $$;
Output:
INFO: template1
It is working example. In your trigger function it could be something like
declare
pk_name text;
pk_value text;
begin
pk_name := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
pk_value := to_jsonb(NEW) ->> pk_name;
-- Do what you want with pk_value here
return null;
end $$;
Related
I am new to using postgresql, I am trying to make a trigger that just inserts in the Employee table and also inserts in the Vacations table, but I don't know how to assign the values, I do it like that in sql but here I really don't know how
CREATE FUNCTION SP_InsertaVacacionesEmpleado() RETURNS TRIGGER
AS
$$
DECLARE _NumeroIdentificacion INTEGER;
DECLARE _FechaEntrada DATE;
BEGIN
SET _NumeroIdentificacion = SELECT NEW.NumeroIdentificacion FROM "Empleado"
SET _FechaEntrada = SELECT NEW.FechaEntrada FROM "Empleado"
INSERT INTO Vacaciones VALUES(_NumeroIdentificacion, _FechaEntrada, '', 0);
RETURN NEW;
END
$$
LANGUAGE plpgsql
As documented in the manual assignment is done using the := operator, e.g.:
some_variable := 42;
However to assign one or more variables from the result of a query, use select into, e.g.:
DECLARE
var_1 INTEGER;
var_2 DATE;
BEGIN
select col1, col2
into var_1, var_2
from some_table
...
However neither of that is necessary in a trigger as you can simply use the reference to the NEW record directly in the INSERT statement:
CREATE FUNCTION sp_insertavacacionesempleado()
RETURNS TRIGGER
AS
$$
BEGIN
INSERT INTO Vacaciones (...)
VALUES (NEW.NumeroIdentificacion, NEW.FechaEntrada , '', 0);
RETURN NEW;
END
$$
LANGUAGE plpgsql;
Note that you need to define a row level trigger for this to work:
create trigger ..
before insert on ...
for each row --<< important!
execute procedure sp_insertavacacionesempleado() ;
I want to create stored procedure to check count of query result. Then if count is > 0 to execute some query to delete records in other table. Below see what i got so far.
CREATE OR REPLACE PROCEDURE myprocedure(tableName VARCHAR, age INT, secondTable VARCHAR)
AS
$$
declare cnt := SELECT COUNT(*) FROM %tableName% WHERE ageID =%id%;
declare result;
BEGIN
EXECUTE cnt;
IF cnt >= 1 THEN
result := SELECT ID FROM %tableName% WHERE ageID =%id%
--remove records from secondTable
EXECUTE DELETE FROM %secondTable% WHERE ID IN (result)
END IF;
COMMIT;
END;
As documented in the manual you can't "reference" a variable with %tableName% and you certainly can not use a variable within a SQL statement for an identifier. You will need to use dynamic SQL.
You also got the DECLARE part completely wrong. You only write the keyword once, and you have to define a data type for the variables.
To create SQL strings that contain identifier, use format() and the %I placeholder to properly deal with identifiers that need quoting.
CREATE OR REPLACE PROCEDURE myprocedure(p_tablename VARCHAR, p_age INT, p_secondtable VARCHAR)
AS
$$
declare
l_sql text;
cnt integer;
BEGIN
l_sql := format('select count(*) from %I where ageid = :1', p_tablename);
EXECUTE l_sql
using p_age
into cnt;
IF cnt >= 1 THEN
l_sql := format('DELETE FROM %I WHERE ID IN (SELECT id FROM %I where ageid = :1)', p_secondtable, p_tablename);
EXECUTE l_sql using p_age;
END IF;
$$
language plpgsql;
But checking for the count before doing the delete is pretty pointless, you can simply that to a single DELETE statement:
CREATE OR REPLACE PROCEDURE myprocedure(p_tablename VARCHAR, p_age INT, p_secondtable VARCHAR)
AS
$$
declare
l_sql text;
cnt integer;
BEGIN
l_sql := format('DELETE FROM %I WHERE id IN (SELECT t.id FROM %I as t where t.ageid = :1)', p_secondtable, p_tablename);
EXECUTE l_sql using p_age;
END IF;
$$
language plpgsql;
Because the DELETE statement won't delete anything if the sub-select doesn't return any rows (which would be the case for cnt = 0). And you only need to query the first table once.
I'm trying to implement a generic trigger procedure to enable a sort a versioning scheme on tables. Tables all have version and current fields. On updates, in some situations based on a condition, i want to create a new version of a row instead of updating the old one. I'm having trouble getting the default value for the primary key field (always id).
Here's what i've done:
CREATE FUNCTION version_trigger() RETURNS trigger AS $$
DECLARE
id_default text;
id_value text;
BEGIN
IF version_condition() THEN
old.current = false;
-- I can read the default value
SELECT column_default INTO id_default
FROM information_schema.columns
WHERE table_name = TG_TABLE_NAME AND column_name = 'id';
-- THIS DOESN'T WORK!
EXECUTE 'SELECT $1' INTO id_value USING id_default;
new.id = id_value;
EXECUTE 'INSERT INTO ' || quote_ident(TG_TABLE_NAME) || ' SELECT ($1).*' USING new;
RETURN old;
END IF;
-- regular UPDATE
RETURN new;
END
$$ LANGUAGE plpgsql;
I'm just missing the step where i read the default value for the id (it's just a nextval() call). Can anyone help out on this?
Thanks in advance!
You cannot use a placeholder for expressions.
If DEFAULT doesn't have any reference to record data, then you can use EXECUTE statement, but little bit different
postgres=# DO $$
DECLARE x text; y text;
BEGIN
x := (SELECT column_default
FROM information_schema.columns
WHERE table_name = 'omega' AND column_name = 'a');
EXECUTE 'SELECT ' || x INTO y;
RAISE NOTICE '%', y;
END;
$$;
NOTICE: 2
DO
In order to check if a column exists I can easily use something similar to this:
SELECT attname FROM pg_attribute
WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'YOURTABLENAME')
AND attname = 'YOURCOLUMNNAME';
However, I run into problems with
SELECT oid FROM pg_class WHERE relname = 'YOURTABLENAME'
When there are several tables with the same name spread out over different schemas as it returns the OID's of all the tables with that name. How do I check if a table within a specific schema contains the column I am after? I'm using Postgres 8.2.
8.2 supports information_schema views. Something along these lines should work. You'll have to supply your own database, schema, table, and column names, of course.
select *
from information_schema.columns
where table_catalog = 'sandbox'
and table_schema = 'public'
and table_name = 'calendar'
and column_name = 'iso_year';
Generic solution, for checking if table or column exists, is using functions (see below).
Here I use also a function to parse the table name.
PS: the easy way to check table is using in your function the ::regclass, see the optional table_exists_v2() function.
CREATE FUNCTION tname_split(
-- Verify the table name, sort, use defaults if necessary, and returns
-- array with indexes: 1=complete name, 2=schema name, 3=table name.
p_tabname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT 'public' -- default schema name
) RETURNS varchar[] AS $func$
DECLARE
p integer;
BEGIN
IF (p_schema IS NULL) THEN p_schema:='public'; END IF;
p := strpos(p_tabname,'.'); -- check schema
IF p=0 AND p_schema='' THEN
RAISE EXCEPTION 'ERROR, schema name default can not be empty';
ELSEIF p=0 THEN
RETURN ARRAY[p_schema||'.'||p_tabname,p_schema,p_tabname];
ELSEIF p=1 THEN
RAISE EXCEPTION 'ERROR, table name initiating by dot';
ELSE
RETURN ARRAY[p_tabname] || string_to_array(p_tabname, '.')::varchar[];
END IF;
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION column_exists(
--
-- Check if a column exists in a table. Returns true if yes.
--
p_colname varchar,
p_tname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT NULL -- default schema name
) RETURNS BOOLEAN AS $func$
DECLARE
t varchar[];
BEGIN
t = lib.tadm_tname_split(p_tname,p_schema);
PERFORM 1 FROM information_schema.COLUMNS
WHERE column_name=$1 AND table_schema=t[2] AND table_name=t[3];
RETURN FOUND;
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION table_exists(
--
-- Check if a table exists, returns true if yes, false if not.
--
p_tname varchar, -- table name or full name (schema.table)
p_schema varchar DEFAULT 'public', -- default schema name
) RETURNS BOOLEAN AS $func$
DECLARE
t varchar[];
BEGIN
t = lib.tadm_tname_split(p_tname,p_schema);
RETURN EXISTS (SELECT tablename FROM pg_tables WHERE schemaname=t[2] AND tablename=t[3]);
END;
$func$ LANGUAGE plpgsql;
CREATE FUNCTION table_exists_v2(p_tname varchar) RETURNS boolean AS $f$
DECLARE
tmp regclass;
BEGIN
tmp := p_tname::regclass; -- do nothing, only parsing regclass
RETURN true;
EXCEPTION WHEN SQLSTATE '3F000' THEN
RETURN false;
END;
$f$ LANGUAGE plpgsql;
For example, in MS-SQL, you can open up a query window and run the following:
DECLARE #List AS VARCHAR(8)
SELECT #List = 'foobar'
SELECT *
FROM dbo.PubLists
WHERE Name = #List
How is this done in PostgreSQL? Can it be done?
Complete answer is located in the official PostgreSQL documentation.
You can use new PG9.0 anonymous code block feature (http://www.postgresql.org/docs/9.1/static/sql-do.html )
DO $$
DECLARE v_List TEXT;
BEGIN
v_List := 'foobar' ;
SELECT *
FROM dbo.PubLists
WHERE Name = v_List;
-- ...
END $$;
Also you can get the last insert id:
DO $$
DECLARE lastid bigint;
BEGIN
INSERT INTO test (name) VALUES ('Test Name')
RETURNING id INTO lastid;
SELECT * FROM test WHERE id = lastid;
END $$;
DO $$
DECLARE
a integer := 10;
b integer := 20;
c integer;
BEGIN
c := a + b;
RAISE NOTICE'Value of c: %', c;
END $$;
You can use:
\set list '''foobar'''
SELECT * FROM dbo.PubLists WHERE name = :list;
That will do
Here's an example of using a variable in plpgsql:
create table test (id int);
insert into test values (1);
insert into test values (2);
insert into test values (3);
create function test_fn() returns int as $$
declare val int := 2;
begin
return (SELECT id FROM test WHERE id = val);
end;
$$ LANGUAGE plpgsql;
SELECT * FROM test_fn();
test_fn
---------
2
Have a look at the plpgsql docs for more information.
I've came across some other documents which they use \set to declare scripting variable but the value is seems to be like constant value and I'm finding for way that can be acts like a variable not a constant variable.
Ex:
\set Comm 150
select sal, sal+:Comm from emp
Here sal is the value that is present in the table 'emp' and comm is the constant value.
Building on #nad2000's answer and #Pavel's answer here, this is where I ended up for my Flyway migration scripts. Handling for scenarios where the database schema was manually modified.
DO $$
BEGIN
IF NOT EXISTS(
SELECT TRUE FROM pg_attribute
WHERE attrelid = (
SELECT c.oid
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE
n.nspname = CURRENT_SCHEMA()
AND c.relname = 'device_ip_lookups'
)
AND attname = 'active_date'
AND NOT attisdropped
AND attnum > 0
)
THEN
RAISE NOTICE 'ADDING COLUMN';
ALTER TABLE device_ip_lookups
ADD COLUMN active_date TIMESTAMP;
ELSE
RAISE NOTICE 'SKIPPING, COLUMN ALREADY EXISTS';
END IF;
END $$;
For use variables in for example alter table:
DO $$
DECLARE name_pk VARCHAR(200);
BEGIN
select constraint_name
from information_schema.table_constraints
where table_schema = 'schema_name'
and table_name = 'table_name'
and constraint_type = 'PRIMARY KEY' INTO name_pk;
IF (name_pk := '') THEN
EXECUTE 'ALTER TABLE schema_name.table_name DROP CONSTRAINT ' || name_pk;
Postgresql does not have bare variables, you could use a temporary table.
variables are only available in code blocks or as a user-interface feature.
If you need a bare variable you could use a temporary table:
CREATE TEMP TABLE list AS VALUES ('foobar');
SELECT dbo.PubLists.*
FROM dbo.PubLists,list
WHERE Name = list.column1;
I had to do something like this
CREATE OR REPLACE FUNCTION MYFUNC()
RETURNS VOID AS $$
DO
$do$
BEGIN
DECLARE
myvar int;
...
END
$do$
$$ LANGUAGE SQL;
You can also simply make a constant query that you use in the actual query:
WITH vars as (SELECT 'foobar' AS list)
SELECT *
FROM dbo.PubLists, vars
WHERE Name = vars.list
Given the popularity, and somewhat incomplete answers I'll provide two solutions.
A do block that won't return rows. You can return rows with a transaction cursor, but it's a bit messy.
A function (that returns rows)
Below I'll use an over-baked example of updating the tweet on the bottom right "blurb" with "hello world".
id (serial)
pub_id (text)
tweet (text)
1
abc
hello world
2
def
blurb
A simple do block
do $$
declare
src_pub_id text;
dst_pub_id text;
src_id int;
dest_id int;
src_tweet text;
begin
src_pub_id := 'abc';
dst_pub_id := 'def';
-- query result into a temp variable
src_id := (select id from tweets where pub_id = src_pub_id);
-- query result into a temp variable (another way)
select tweet into src_tweet from tweets where id = src_id;
dest_id := (select id from tweets where pub_id = dst_pub_id);
update tweets set tweet=src_tweet where id = dest_id;
end $$ language plpgsql; -- need the language to avoid ERROR 42P13
A function
create or replace function sync_tweets(
src_pub_id text, -- function arguments
dst_pub_id text
) returns setof tweets as -- i.e. rows. int, text work too
$$
declare
src_id int; -- temp function variables (not args)
dest_id int;
src_tweet text;
begin
-- query result into a temp variable
src_id := (select id from tweets where pub_id = src_pub_id);
-- query result into a temp variable (another way)
select tweet into src_tweet from tweets where id = src_id;
dest_id := (select id from tweets where pub_id = dst_pub_id);
update tweets set tweet=src_tweet where id = dest_id;
return query -- i.e. rows, return 0 with return int above works too
select * from tweets where pub_id in (src_pub_id, dst_pub_id);
end
$$ language plpgsql; -- need the language to avoid ERROR 42P13
-- Run it!
select * from sync_tweets('abc', 'def');
-- Optional drop if you don't want the db to keep your function
drop function if exists sync_tweets(text, text);
/*
Outputs
__________________________________________________
| id (serial) | pub_id (text) | tweet (text) |
|---------------|-----------------|----------------|
| 1 | abc | hello world |
| 2 | def | blurb |
--------------------------------------------------
*/