trying to create a script with a subblock in postgres - postgresql

I am trying create this script where if it matches the current database , then do a chunk of work inside a sub block. As this is my first attempt, i cannot get this to work. Any thoughts?
DO
$do$
DECLARE
database CONSTANT text[] := array['prd1', 'prd2'];
BEGIN
IF current_database() = any(database)
THEN
**--execute the below sub block**
DECLARE
v_sql text;
BEGIN
v_sql :=
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this
WHERE rolname = 'dave') THEN
create role dave encrypted password 'md502bbddbc560b6470b360219ac95c13e2';
create schema authorization dave;
END IF;
END
);
-- end of sub block
END IF;
END
$do$;
SQL Error [42601]: ERROR: syntax error at or near "NOT" Position:
231
What i want do is a like where it does alot of actions in a sub block:
DO
$do$
DECLARE
database CONSTANT text[] := array['prd1', 'prd2'];
BEGIN
IF current_database() = any(database)
THEN
**--execute the below sub block**
DECLARE
v_sql text;
BEGIN
v_sql :=
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this
WHERE rolname = 'dave') THEN
create role dave encrypted password 'md502bbddbc560b6470b360219ac95c13e2';
create schema authorization dave;
END IF;
END
do
$$
begin
execute format('grant connect, temporary on database %I to %I', current_database(), 'user_monitor');
end;
$$;
CREATE OR REPLACE FUNCTION test.log_ddl()
RETURNS event_trigger AS $$
DECLARE
audit_query TEXT;
r RECORD;
BEGIN
[...]
END;
$$ LANGUAGE plpgsql;
**-- end of sub block**
END IF;
END
$do$;

Why use sub-block when you can use the IF conditional? 2) Why v_sql := ...? I see no point in assigning the query to a variable. Again all you want is to take CREATE actions based on the IF condition. –
Adrian Klaver

Related

Triggers in Postgres: Access NEW fields by name at runtime

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 $$;

Postgresql dynamic cursor (for record in EXECUTE), field access

Suppose the block below:
do $$
declare
v_table_name text:='table_name';
v_column_name text:='column_name';
v_record record;
v_sql_dynamic text;
v_enum_xml xml;
v_string text;
begin
v_sql_dynamic:=format('select %s from %s',v_column_name,v_table_name);
FOR v_record IN EXECUTE v_sql_dynamic
LOOP
raise notice 'field with name % has a value of %',v_column_name,***?????***;
END LOOP;
end;
$$ language plpgsql
How do I get access to the field of v_record in the way like: v_record.v_column_name?
See Abelisto's comment. While converting the value to text anyway, to_json() is good enough and slightly faster.
DO
$$
DECLARE
v_table_name text := 'table_name';
v_column_name text := 'column_name';
v_sql_dynamic text := format('select %s from %s', v_column_name, v_table_name);
v_record record;
BEGIN
FOR v_record IN EXECUTE v_sql_dynamic
LOOP
RAISE NOTICE 'field named % has value %.'
, v_column_name, to_json(v_record) ->> v_column_name;
END LOOP;
END
$$ LANGUAGE plpgsql;
Related:
How to get the value of a dynamically generated field name in PL/pgSQL

Dynamically generated CURSOR in Postgresql

I have got a cursor, it is pointing to a SELECT, but this select is generated dynamically. I want to assign the statement after the declarement.
I have done an example working and another example NOT working. This is a simple example to print some data only.
This is the table:
CREATE TABLE public.my_columns (
id serial NOT NULL,
"name" varchar(30) NOT NULL,
/* Keys */
CONSTRAINT my_columns_pkey
PRIMARY KEY (id)
) WITH (
OIDS = FALSE
);
CREATE INDEX my_columns_index01
ON public.my_columns
("name");
INSERT INTO public.my_columns
("name")
VALUES
('name1'),
('name2'),
('name3'),
('name4'),
('name5'),
('name6');
This is the function(I have put the working code and the code not working):
CREATE OR REPLACE FUNCTION public.dynamic_table
(
)
RETURNS text AS $$
DECLARE
v_sql_dynamic varchar;
--NOT WORKING:
--db_c CURSOR IS (v_sql_dynamic::varchar);
--WORKING:
db_c CURSOR IS (SELECT id, name from public.my_columns);
db_rec RECORD;
BEGIN
v_sql_dynamic := 'SELECT id, name from public.my_columns';
FOR db_rec IN db_c LOOP
RAISE NOTICE 'NAME: %', db_rec.name;
END LOOP;
RETURN 'OK';
EXCEPTION WHEN others THEN
RETURN 'Error: ' || SQLERRM::text || ' ' || SQLSTATE::text;
END;
$$ LANGUAGE plpgsql;
Any ideas?
Thank you.
Do you really need the explicit cursor? If you need iterate over dynamic SQL, then you can use FOR IN EXECUTE. It is loop over implicit (internal) cursor for dynamic SQL
FOR db_rec IN EXECUTE v_sql_dynamic
LOOP
..
END LOOP
Little bit more complex solution is described in documentation - OPEN FOR EXECUTE:
do $$
declare r refcursor; rec record;
begin
open r for execute 'select * from pg_class';
fetch next from r into rec;
while found
loop
raise notice '%', rec;
fetch next from r into rec;
end loop;
close r;
end $$;
With this kind of cursor, you cannot to use FOR IN

How to keep looping even error happend?

I wrote a PL/pgsql to batch create index on tables
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r INTEGER;
BEGIN
FOR r IN 1..1000
LOOP
EXECUTE format(
' CREATE INDEX idx_abc_id_' || r::text ||
' ON abc_id_' || r::text ||
' USING btree
(key);');
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
it has one problem, if partition abc_500 doesn't exist, then the how create index function will fail and do nothing.
How to make loop keep going through even if create_index made an error on one of the table in between?
I think a better approach would be to not hardcode the number for the loop, but iterate over the existing tables:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
When you use the format() function is better to use the proper place holders for identifiers.
If you also want to ignore any error when creating the index on an existing table, you need to catch the exception and ignore it:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
msg text;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
BEGIN
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
RAISE NOTICE 'Could not create index for: %, %', r.idx_nr, msg;
END;
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;

For loop with dynamic table name in Postgresql 9.1?

I have a plpgslq function which does some data processing and would like to write a for loop, however my table name is not known at design time. Is there any possible way to achieve this? Here is sample code snippet of what I want to achieve:
-- Function: check_data()
-- DROP FUNCTION check_data();
CREATE OR REPLACE FUNCTION check_data()
RETURNS character varying AS
$BODY$declare
dyn_rec record;
tbl_name record;
begin
-- sample dynamic tables
tbl_name := 'cars';
tbl_name := 'trucks';
tbl_name := 'bicycles';
for dyn_rec in select * from format($$s%$$,tbl_name) loop
raise notice 'item is %',dyn_rec.item_no;
end loop;
return 'Processing Ok';
end;$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION check_data()
OWNER TO postgres;
You cannot use a variable as table or column identifier in plpgsql embedded SQL ever. A solution is dynamic SQL - EXECUTE or FOR IN EXECUTE statements:
DO $$
DECLARE
tables text[] = ARRAY['table1','table2'];
table_name text;
rec record;
BEGIN
FOREACH table_name IN ARRAY tables
LOOP
FOR r IN EXECUTE format('SELECT * FROM %I', table_name)
LOOP
RAISE NOTICE '%', rec;
END LOOP;
END LOOP;
END; $$