PL/pgSQL CREATE or REPLACE within EXECUTE - postgresql

I have the following script to dynamically create views into a PostgreSQL database.
CREATE OR REPLACE FUNCTION cs_refresh_mviews() RETURNS integer AS $$
DECLARE
mviews RECORD;
query text;
park_name text;
ppstatements int;
BEGIN
RAISE NOTICE 'Creating views...';
FOR mviews IN SELECT name FROM "Canadian_Parks" LOOP
park_name := mviews.name;
RAISE NOTICE 'Creating or replace view %s...', mviews.name;
query := 'CREATE OR REPLACE VIEW %_view AS
SELECT * from "Canadian_Parks" where name=''%'';
ALTER TABLE %_view OWNER TO postgres', park_name, park_name, park_name;
-- RAISE NOTICE query;
EXECUTE query;
END LOOP;
RAISE NOTICE 'Done refreshing materialized views.';
RETURN 1;
END;
$$ LANGUAGE plpgsql;
I have confirmed integrity of the string, such as
CREATE OR REPLACE VIEW Saguenay_St__Lawrence_view AS
SELECT * from "Canadian_Parks" where name='Saguenay_St__Lawrence';
ALTER TABLE Saguenay_St__Lawrence_view OWNER TO postgres
assigned to the query variable by manually submitting this to the database and getting a successful response.
However, if I attempt to execute the function using
SELECT cs_refresh_mviews();
the followig error is displayed:
ERROR: query "SELECT 'CREATE OR REPLACE VIEW %_view AS SELECT * from "Canadian_Parks" where name=''%''; ALTER TABLE %_view OWNER TO postgres', park_name, park_name, park_name" returned 4 columns
CONTEXT: PL/pgSQL function "cs_refresh_mviews" line 32 at assignment
********** Error **********
ERROR: query "SELECT 'CREATE OR REPLACE VIEW %_view AS SELECT * from "Canadian_Parks" where name=''%''; ALTER TABLE %_view OWNER TO postgres', park_name, park_name, park_name" returned 4 columns
SQL state: 42601
Context: PL/pgSQL function "cs_refresh_mviews" line 32 at assignment
Why has this been converted to a SELECT statement, instead of a pure CREATE?

You setup is pretty twisted. Why would you save part of the name of a view in a composite type of a table instead of saving it in a plain text column?
Anyhow, it could work like this:
Setup matching question:
CREATE SCHEMA x; -- demo in test schema
SET search_path = x;
CREATE TYPE mviews AS (id int, name text); -- composite type used in table
CREATE TABLE "Canadian_Parks" (name mviews);
INSERT INTO "Canadian_Parks"(name) VALUES
('(1,"canadian")')
,('(2,"islandic")'); -- composite types, seriously?
SELECT name, (name).* from "Canadian_Parks";
CREATE OR REPLACE FUNCTION cs_refresh_mviews()
RETURNS int LANGUAGE plpgsql SET search_path = x AS -- search_path for test
$func$
DECLARE
_parkname text;
BEGIN
FOR _parkname IN SELECT (name).name FROM "Canadian_Parks" LOOP
EXECUTE format('
CREATE OR REPLACE VIEW %1$I AS
SELECT * FROM "Canadian_Parks" WHERE (name).name = %2$L;
ALTER TABLE %1$I OWNER TO postgres'
, _parkname || '_view', _parkname);
END LOOP;
RETURN 1;
END
$func$;
SELECT cs_refresh_mviews();
DROP SCHEMA x CASCADE; -- clean up
Major points
As you are executing text with execute, you need to safeguard against SQL injection. I use the format() function for identifiers and the literal
I use the syntax SELECT (name).name to cope with your weird setup and extract the name we need right away.
Similarly, the VIEW needs to read WHERE (name).name = .. to work in this setup.
I removed a lot of noise that is irrelevant to the question.
It's also probably pointless to have the function RETURN 1. Just define the function with RETURNS void. I kept it, though, to match the question.
Untangled setup
How it probably should be:
CREATE SCHEMA x;
SET search_path = x;
CREATE TABLE canadian_parks (id serial primary key, name text);
INSERT INTO canadian_parks(name) VALUES ('canadian'), ('islandic');
SELECT * from canadian_parks;
CREATE OR REPLACE FUNCTION cs_refresh_mviews()
RETURNS void LANGUAGE plpgsql SET search_path = x AS
$func$
DECLARE
parkname text;
BEGIN
FOR parkname IN SELECT name FROM canadian_parks LOOP
EXECUTE format('
CREATE OR REPLACE VIEW %1$I AS
SELECT * FROM canadian_parks WHERE name = %2$L;
ALTER TABLE %1$I OWNER TO postgres'
, parkname || '_view', parkname);
END LOOP;
END
$func$;
SELECT cs_refresh_mviews();
DROP SCHEMA x CASCADE;

You've misunderstood usage of commas in assignment expression.
It turns query to array (RECORD) instead of scalar.
Use concatenation:
park_name := quote_ident(mviews.name||'_view');
query := 'CREATE OR REPLACE VIEW '||park_name||' AS SELECT * from "Canadian_Parks" where name='||quote_literal(mviews.name)||'; ALTER TABLE '||park_name||' OWNER TO postgres';

Related

How to insert declared type variable into table | Postgress

I have been working on creating a store procedure that will select data from a table, do some modification to that data, and then I need to insert that modified data into the same table. Take an example my table name is student. My procedure looks like below:
create or replace procedure student_create(p_code varchar)
language plpgsql
as $$
declare
v_student public.student;
begin
select * into v_student from student where code = p_code and is_latest_version is true;
raise notice 'Value: %', v_student;
v_student.id = uuid_generate_v4();
v_student.version_created_at = now();
v_student.version_updated_at = v_student.version_created_at;
raise notice 'Value: %', v_student;
INSERT INTO public.student VALUES(v_student);
end;$$
I am getting errors while calling this procedure:
ERROR: column "id" is of type uuid but expression is of type hotel
LINE 1: INSERT INTO public.hotel VALUES(v_hotel)
I know I can make insert statements like I can get each value from the variable and set it like
INSERT INTO public.student VALUES(v_student.id, v_student.code, v_student.name);
But I don't want to do that because it will become tightly coupled and later if I add another column into the table then I need to add that column into this procedure as well.
Does anyone have idea how can I insert the declared type variable directly into table.
There is no table type, there is only row composite type. Check manual 43.3.4. Row Types.
use row type.
create or replace procedure student_create(p_code text)
language plpgsql
as $$
declare
v_student public.student
begin
for v_student in select * from student where code = p_code and is_latest_version is true
loop
v_student.id = uuid_generate_v4();
v_student.version_created_at = now();
v_student.version_updated_at = v_student.version_created_at;
v_student.is_latest_version = true;
v_student.code = p_code;
INSERT INTO student VALUES(v_student.*);
end loop;
end;$$;
call it: call student_create('hello');
3. use update clause directly.
create or replace procedure student_create_1(p_code text)
language plpgsql as $$
BEGIN
with a as ( select uuid_generate_v4() as id ,
now() as version_created_at,
now() as version_updated_at,
p_code as "code" from student
where code = p_code and is_latest_version is true)
INSERT INTO student(id, version_created_at, version_updated_at, code)
select a.id, a.version_created_at,a.version_updated_at,a."code" from a;
end
$$;
call it: call student_create_1('hello');
fiddle code: here

I want fetch data from different different table name using postgresql function

I have 30 state wise data tables. Table name like aa_shg_detail, ab_shg_detail, ac_shg_detail.
I have also main state table in which state short names and state codes are stored. I have created 2 postgresql functions getTableName(Code text) and getDataByTable().
In the first function I pass the state code so it fetches the state short name and short name concat with _shg_detail String and prepare full table name and return it. Example: If I pass state code 2 the query fetch state short name based on state code 2 from the state's main table. The state short name is 'ab' for state code 2 so after concat state short name with _shg_detail first function return ab_shg_detail table name.
Second function gets the table name from first function and fetch data from that table. But I am getting error in the second function.
CREATE OR REPLACE FUNCTION getTableName(code text)
RETURNS text
AS $$
select concat(lower(state_short_name), '_shg_detail') from main_state where state_code = code))
$$
LANGUAGE sql;
CREATE OR REPLACE FUNCTION getDataByTable()
RETURNS text AS $$
DECLARE
tablename text;
BEGIN
tablename := gettablename('2');
RETURN (select shg_code from tablename);
END;
$$ LANGUAGE plpgsql;
When I execute a second function select getDataByTable() then I am getting this error every time:
ERROR: relation "tablename" does not exist
LINE 1: SELECT (select shg_code from tableName)
You need dynamic SQL for that:
CREATE OR REPLACE FUNCTION getDataByTable()
RETURNS text AS $$
DECLARE
tablename text;
l_result text;
BEGIN
tablename := gettablename('2');
execute format('select shg_code from %I', tablename)
into l_result;
RETURN l_result;
END;
$$ LANGUAGE plpgsql;
The %I placeholder of the format() function properly deals with quoting of identifiers if needed.

Loop on rows produced by SELECT - looping variable does not exist?

The function is created fine, but when I try to execute it, I get this error:
ERROR: relation "column1" does not exist
SQL state: 42P01
Context: SQL statement "ALTER TABLE COLUMN1 ADD COLUMN locationZM geography (POINTZM, 4326)"
PL/pgSQL function addlocationzm() line 6 at SQL statement
Code:
CREATE OR REPLACE FUNCTION addlocationZM()
RETURNS void AS
$$
DECLARE
COLUMN1 RECORD;
BEGIN
FOR COLUMN1 IN SELECT f_table_name FROM *schema*.geography_columns WHERE type LIKE 'Point%' LOOP
ALTER TABLE COLUMN1 ADD COLUMN locationZM geography (POINTZM, 4326);
END LOOP;
END;
$$
LANGUAGE 'plpgsql';
SELECT addlocationZM()
I'm probably just being dumb, but I've been at this for a while now and I just can't get it. The SELECT f_table_name ... statement executed on its own returns 58 rows of a single column, each of which is the name of a table in my schema. The idea of this is to create a new column, type PointZM, in each table pulled by the SELECT.
The function would work like this:
CREATE OR REPLACE FUNCTION addlocationZM()
RETURNS void AS
$func$
DECLARE
_tbl text;
BEGIN
FOR _tbl IN
SELECT f_table_name FROM myschema.geography_columns WHERE type LIKE 'Point%'
LOOP
EXECUTE
format('ALTER TABLE %I ADD COLUMN location_zm geography(POINTZM, 4326)', _tbl);
END LOOP;
END
$func$ LANGUAGE plpgsql;
Note how I use a simple text variable to simplify matters. You don't need the record to begin with.
If it's a one-time operation, use a DO command instead of creating a function:
DO
$do$
BEGIN
EXECUTE (
SELECT string_agg(
format(
'ALTER TABLE %I ADD COLUMN location_zm geography(POINTZM, 4326);'
, f_table_name)
, E'\n')
FROM myschema.geography_columns
WHERE type LIKE 'Point%'
);
END
$do$;
This is concatenating a single string comprised of all commands (separated with ;) for a single EXECUTE.
Or, especially while you are not familiar with plpgsql and dynamic SQL, just generate the commands, copy/paste the result and execute as 2nd step:
SELECT 'ALTER TABLE '
|| quote_ident(f_table_name)
|| ' ADD COLUMN locationZM geography(POINTZM, 4326);'
FROM myschema.geography_columns
WHERE type LIKE 'Point%';
(Demonstrating quote_ident() this time.)
Related:
Table name as a PostgreSQL function parameter
Aside: Unquoted CaMeL-case identifiers like locationZM or your function name addlocationZM may not be such a good idea:
Are PostgreSQL column names case-sensitive?

Only perform update if column exists

Is it possible to execute an update conditionally if a column exists?
For instance, I may have a column in a table and if that column exists I want that update executed, otherwise, just skip it (or catch its exception).
You can do it inside a function. If you don't want to use the function later you can just drop it afterwards.
To know if a column exists in a certain table, you can try to fetch it using a select(or a perform, if you're gonna discard the result) in information_schema.columns.
The query bellow creates a function that searches for a column bar in a table foo, and if it finds it, updates its value. Later the function is run, then droped.
create function conditional_update() returns void as
$$
begin
perform column_name from information_schema.columns where table_name= 'foo' and column_name = 'bar';
if found then
update foo set bar = 12345;
end if;
end;
$$ language plpgsql;
select conditional_update();
drop function conditional_update();
With the following table as example :
CREATE TABLE mytable (
idx INT
,idy INT
);
insert into mytable values (1,2),(3,4),(5,6);
you can create a custom function like below to update:
create or replace function fn_upd_if_col_exists(_col text,_tbl text,_val int) returns void as
$$
begin
If exists (select 1
from information_schema.columns
where table_schema='public' and table_name=''||_tbl||'' and column_name=''||_col||'' ) then
execute format('update mytable set '||_col||'='||_val||'');
raise notice 'updated';
else
raise notice 'column %s doesn''t exists on table %s',_col,_tbl;
end if;
end;
$$
language plpgsql
and you can call this function like:
select fn_upd_if_col_exists1('idz','mytable',111) -- won't update raise "NOTICE: column idz deosnt exists on table mytables"
select fn_upd_if_col_exists1('idx','mytable',111) --will upadate column idx with value 1111 "NOTICE: updated"

Syntax error at CREATE TRIGGER query

I have this code, to add trigger for a View in my postgresql database
CREATE OR REPLACE FUNCTION changeCityProc() RETURNS TRIGGER AS $changeCity$
BEGIN
UPDATE vvs.tb_company SET city = "LAL" WHERE cardpresso.cardinfo.tb_company_city = vvs.tb_company.city;
RETURN null;
END;
$changeCity$ LANGUAGE plpgsql;
CREATE TRIGGER mytrigger
INSTEAD OF UPDATE ON
cardpresso.cardinfo FOR EACH ROW EXECUTE PROCEDURE changeCityProc();
pgAdmin says "syntax error at or near "INSTEAD""
Let's simplify this. First, the two schemas.
CREATE SCHEMA cardpresso;
CREATE SCHEMA vvs;
Next, the simplest possible table.
CREATE TABLE vvs.tb_company
(
city character varying(25)
);
INSERT INTO vvs.tb_company VALUES ('foo');
And a fairly useless view.
CREATE VIEW cardpresso.cardinfo AS
SELECT 'test' as tb_company_city UNION ALL
SELECT 'Orem' UNION ALL
SELECT 'Wibble'
;
Simplify the function. Warning: This will destructively update the table. Be careful if you copy stuff from this function into something headed for production.
CREATE OR REPLACE FUNCTION changecityproc()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE vvs.tb_company SET city = 'bar';
RETURN null;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Finally, the trigger.
CREATE TRIGGER mytrigger
INSTEAD OF UPDATE
ON cardpresso.cardinfo
FOR EACH ROW
EXECUTE PROCEDURE changecityproc();
Now, if we update the view, we'd expect all the rows in vvs.tb_company to change to 'bar'.
select * from vvs.tb_company;
city
--
foo
update cardpresso.cardinfo
set tb_company_city = 'wibble';
select * from vvs.tb_company;
city
--
bar
So it looks like the problem is in the UPDATE statement of your function.
UPDATE vvs.tb_company SET city = "LAL"
WHERE cardpresso.cardinfo.tb_company_city = vvs.tb_company.city;
I'm not certain what you intended that statement to do, but it's not valid in PostgreSQL, regardless of whether "LAL" is supposed to be a column name or a string literal. As a string literal, 'LAL', it will raise an error.
ERROR: missing FROM-clause entry for table "cardinfo"
LINE 2: WHERE cardpresso.cardinfo.tb_company_city = vvs.tb_compa...