How to concat regclass name with a string in postgres query - sql

I want to create indexes on partition tables using this function. Select query gives me all partitioned tables of test.t and then I iterate over its names and create indexes.
CREATE OR REPLACE FUNCTION test.create_index_test() RETURNS void AS
$BODY$
DECLARE
f record;
BEGIN
FOR f IN
SELECT inhrelid::regclass-- optionally cast to text
FROM pg_catalog.pg_inherits
WHERE inhparent = 'test.t'::regclass
loop
CREATE INDEX concat(f::text,'_geom_index') ON f USING gist (geom);
-- can do some processing here
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
But this is giving me error in concat function
ERROR: syntax error at or near "("
LINE 11: CREATE INDEX concat(f,'_geom_index') ON f USING gist (geom)...

DDL statements doesn't support parametrizations - so you cannot to use variables there.
You need to use dynamic SQL - EXECUTE statement
CREATE OR REPLACE FUNCTION test.create_index_test() RETURNS void AS
$BODY$
DECLARE
r record;
BEGIN
FOR r IN
SELECT inhrelid::regclass AS fullpath, c.relname
FROM pg_catalog.pg_inherits
JOIN pg_class c ON c.oid = inhrelid
WHERE inhparent = 'test.t'::regclass
LOOP
EXECUTE FORMAT('CREATE INDEX %I ON %s USING gist(geom)',
r.relname || '_geom_index',
r.fullpath);
-- the index name is optional in postgres, so it can be simplified
-- EXECUTE FORMAT('CREATE INDEX ON %s USING gist(geom)',
-- r.fullpath);
-- can do some processing here
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;

Related

How to prevent table creation without primary key in Postgres?

I would like to enforce a rule such that when people are creating table without primary key, it throws an error. Is it possible to be done from within pgdb?
DROP EVENT TRIGGER trig_test_event_trigger_table_have_primary_key;
CREATE OR REPLACE FUNCTION test_event_trigger_table_have_primary_key ()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
object_types text[];
table_name text;
BEGIN
FOR obj IN
SELECT
*
FROM
pg_event_trigger_ddl_commands ()
LOOP
RAISE NOTICE 'classid: % objid: %,object_type: %
object_identity: % schema_name: % command_tag: %' , obj.classid , obj.objid , obj.object_type , obj.object_identity , obj.schema_name , obj.command_tag;
IF obj.object_type ~ 'table' THEN
table_name := obj.object_identity;
END IF;
object_types := object_types || obj.object_type;
END LOOP;
RAISE NOTICE 'table name: %' , table_name;
IF EXISTS (
SELECT
FROM
pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY (i.indkey)
WHERE
i.indisprimary
AND i.indrelid = table_name::regclass) IS FALSE THEN
RAISE EXCEPTION ' no primary key, this table not created';
END IF;
END;
$$;
CREATE EVENT TRIGGER trig_test_event_trigger_table_have_primary_key ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE FUNCTION test_event_trigger_table_have_primary_key ();
demo:
DROP TABLE a3;
DROP TABLE a4;
DROP TABLE a5;
CREATE TABLE a3 (
a int
);
CREATE TABLE a4 (
a int PRIMARY KEY
);
CREATE TABLE a5 (
a1 int UNIQUE
);
Only table a4 will be created.
related post: PL/pgSQL checking if a row exists
https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
EDIT: Someone else has answered regarding how to test the existence of primary keys, which completes Part 2 below. You will have to combine both answers for the full solution.
The logic fits inside several event triggers (also see documentation for the create command).
First point to note is the DDL commands this can apply to, all documented here.
Part 1: CREATE TABLE AS & SELECT INTO
If I am not wrong, CREATE TABLE AS and SELECT INTO never add constraints on the created table, they must be blocked with an event trigger that always raises an exception.
CREATE OR REPLACE FUNCTION block_ddl()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
BEGIN
RAISE EXCEPTION 'It is forbidden to create tables using command: %', tg_tag ;
END;
$$;
CREATE EVENT TRIGGER AdHocTables_forbidden
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE AS', 'SELECT INTO')
EXECUTE FUNCTION block_ddl();
Note your could define the trigger to be ON ddl_command_start`. It makes it a little bit faster but does not go well with the full code I posted at the end.
See the next, less straightforward part for the rest of the explanations.
Part 2: Regular CREATE TABLE & ALTER TABLE
This case is more complex, as we want to block only some commands but not all.
The function and event trigger below do:
Output the whole command being passed.
Break the command into its subparts.
To do it, it uses the pg_event_trigger_ddl_commands() (documentation here), which BTW is the reason why this trigger had to be on ddl_command_end.
You will note that when adding a primary key, a CREATE INDEX is caught too.
In the case of the function below, raises an exception to block the creation in all cases (so you can test it without dropping the table you create every time).
Here is the code:
CREATE OR REPLACE FUNCTION pk_enforced()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
DECLARE r RECORD;
BEGIN
RAISE NOTICE 'Caught command %', (SELECT current_query());
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
RAISE NOTICE 'Caught inside command % (%)', r.command_tag, r.object_identity;
END LOOP;
RAISE EXCEPTION 'Blocking the Creation';
END;
$$;
CREATE EVENT TRIGGER pk_is_mandatory
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE')
EXECUTE FUNCTION pk_enforced();
Additional notes:
You can prevent these constraints from being enforced on a temporary table by tested the schema_name is not pg_temp. The full code, including this test and with credit to jian for the function he posted:
CREATE OR REPLACE FUNCTION public.pk_enforced()
RETURNS event_trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
obj RECORD;
table_name text;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands ()
LOOP
IF obj.schema_name = 'pg_temp' THEN
return;
END IF;
IF obj.object_type ~ 'table' THEN
table_name := obj.object_identity;
END IF;
END LOOP;
IF NOT EXISTS (
SELECT
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 OR i.indisunique)) THEN
RAISE EXCEPTION 'A primary key or a unique constraint is mandatory to perform % on %.', tg_tag, obj.object_identity;
END IF;
END;
$BODY$;
CREATE OR REPLACE FUNCTION public.block_ddl()
RETURNS event_trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
obj RECORD;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands ()
LOOP
IF obj.schema_name = 'pg_temp' THEN
return;
END IF;
END LOOP;
RAISE EXCEPTION 'DDL command ''%'' is blocked.', tg_tag ;
END;
$BODY$;
CREATE EVENT TRIGGER pk_is_mandatory ON DDL_COMMAND_END
WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE')
EXECUTE PROCEDURE public.pk_enforced();
CREATE EVENT TRIGGER adhoctables_forbidden ON DDL_COMMAND_END
WHEN TAG IN ('CREATE TABLE AS', 'SELECT INTO')
EXECUTE PROCEDURE public.block_ddl();

Relation does not exist PLPGSQL

I am trying to create a view inside the function using plpgsql which returns the x column of the "small" table which is defined as (x integer,y integer).
create or replace function skyline_naive2(dataset text) returns setof integer as
$$
declare
fullx text;
begin
fullx = dataset||'_skyline_naive2';
execute format('create view %s as select x,y from %s',fullx,dataset);
return query select x from fullx;
end
$$ language plpgsql;
select * from skyline_naive2('small');
It returns "relation fullx does not exist"
I understand that it is because there is no fullx relation, but I want to call the view using the variable name.
Any help will be
Use dynamic SQL for select (as you have used for create):
create or replace function skyline_naive2(dataset text) returns setof integer as
$$
declare
fullx text;
begin
fullx = dataset||'_skyline_naive2';
execute format('create view %I as select x,y from %I',fullx,dataset);
return query execute format('select x from %I', fullx);
end
$$ language plpgsql;
You need to EXECUTE your dynamic query:
RETURN QUERY EXECUTE 'SELECT x FROM ' || fullx;

How can I refer to a variable in postgresql dynamic SQL?

I'm trying to write a PostgreSQL function for table upserts that can be used for any table. My starting point is taken from a concrete function for a specific table type:
CREATE TABLE doodad(id BIGINT PRIMARY KEY, data JSON);
CREATE OR REPLACE FUNCTION upsert_doodad(d doodad) RETURNS VOID AS
$BODY$
BEGIN
LOOP
UPDATE doodad
SET id = (d).id, data = (d).data
WHERE id = (d).id;
IF found THEN
RETURN;
END IF;
-- does not exist, or was just deleted.
BEGIN
INSERT INTO doodad SELECT d.*;
RETURN;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing, and loop to try the update again
END;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
The dynamic SQL version of this for any table that I've come up with is here:
SQL Fiddle
CREATE OR REPLACE FUNCTION upsert(target ANYELEMENT) RETURNS VOID AS
$$
DECLARE
attr_name NAME;
col TEXT;
selectors TEXT[];
setters TEXT[];
update_stmt TEXT;
insert_stmt TEXT;
BEGIN
FOR attr_name 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 = format_type(pg_typeof(target), NULL)::regclass
AND i.indisprimary
LOOP
selectors := array_append(selectors, format('%1$s = target.%1$s', attr_name));
END LOOP;
FOR col IN SELECT json_object_keys(row_to_json(target))
LOOP
setters := array_append(setters, format('%1$s = (target).%1$s', col));
END LOOP;
update_stmt := format(
'UPDATE %s SET %s WHERE %s',
pg_typeof(target),
array_to_string(setters, ', '),
array_to_string(selectors, ' AND ')
);
insert_stmt := format('INSERT INTO %s SELECT (target).*', pg_typeof(target));
LOOP
EXECUTE update_stmt;
IF found THEN
RETURN;
END IF;
BEGIN
EXECUTE insert_stmt;
RETURN;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
When I attempt to use this function, I get an error:
SELECT * FROM upsert(ROW(1,'{}')::doodad);
ERROR: column "target" does not exist: SELECT * FROM upsert(ROW(1,'{}')::doodad)
I tried changing the upsert statement to use placeholders, but I can't figure out how to invoke it using the record:
EXECUTE update_stmt USING target;
ERROR: there is no parameter $2: SELECT * FROM upsert(ROW(1,'{}')::doodad)
EXECUTE update_stmt USING target.*;
ERROR: query "SELECT target.*" returned 2 columns: SELECT * FROM upsert(ROW(1,'{}')::doodad)
I feel really close to a solution, but I can't figure out the syntax issues.
Short answer: you can't.
Variable substitution does not happen in the command string given to EXECUTE or one of its variants. If you need to insert a varying value into such a command, do so as part of constructing the string value, or use USING, as illustrated in Section 40.5.4. 1
Longer answer:
SQL statements and expressions within a PL/pgSQL function can refer to variables and parameters of the function. Behind the scenes, PL/pgSQL substitutes query parameters for such references. 2
This was the first important piece to the puzzle: PL/pgSQL does magic transformations on function parameters that turn them into variable substitutions.
The second was that fields of variable substitutions can referenced:
Parameters to a function can be composite types (complete table rows). In that case, the corresponding identifier $n will be a row variable, and fields can be selected from it, for example $1.user_id. 3
This excerpt confused me, because it referred to function parameters, but knowing that function parameters are implemented as variable substitutions under the hood, it seemed that I should be able to use the same syntax in EXECUTE.
These two facts unlocked the solution: use the ROW variable in the USING clause, and dereference its fields in the dynamic SQL. The results (SQL Fiddle):
CREATE OR REPLACE FUNCTION upsert(v_target ANYELEMENT)
RETURNS SETOF ANYELEMENT AS
$$
DECLARE
v_target_name TEXT;
v_attr_name NAME;
v_selectors TEXT[];
v_colname TEXT;
v_setters TEXT[];
v_update_stmt TEXT;
v_insert_stmt TEXT;
v_temp RECORD;
BEGIN
v_target_name := format_type(pg_typeof(v_target), NULL);
FOR v_attr_name 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 = v_target_name::regclass
AND i.indisprimary
LOOP
v_selectors := array_append(v_selectors, format('t.%1$I = $1.%1$I', v_attr_name));
END LOOP;
FOR v_colname IN SELECT json_object_keys(row_to_json(v_target))
LOOP
v_setters := array_append(v_setters, format('%1$I = $1.%1$I', v_colname));
END LOOP;
v_update_stmt := format(
'UPDATE %I t SET %s WHERE %s RETURNING t.*',
v_target_name,
array_to_string(v_setters, ','),
array_to_string(v_selectors, ' AND ')
);
v_insert_stmt = format('INSERT INTO %I SELECT $1.*', v_target_name);
LOOP
EXECUTE v_update_stmt INTO v_temp USING v_target;
IF v_temp IS NOT NULL THEN
EXIT;
END IF;
BEGIN
EXECUTE v_insert_stmt USING v_target;
EXIT;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
RETURN QUERY SELECT v_target.*;
END;
$$
LANGUAGE plpgsql;
For writeable CTE fans, this is trivially convertible to CTE form:
v_cte_stmt = format(
'WITH up as (%s) %s WHERE NOT EXISTS (SELECT 1 from up t WHERE %s)',
v_update_stmt,
v_insert_stmt,
array_to_string(v_selectors, ' AND '));
LOOP
BEGIN
EXECUTE v_cte_stmt USING v_target;
EXIT;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
RETURN QUERY SELECT v_target.*;
NB: I have done zero performance testing on this solution, and I am relying on the analysis of others for its correctness. For now it appears to run correctly on PostgreSQL 9.3 in my development environment. YMMV.

Sequence name taken from variable

How do I create a new sequence taking its name is from a variable?
Let's take a look at the following example:
CREATE OR REPLACE FUNCTION get_value(_name_part character varying)
RETURNS INTEGER AS
$BODY$
DECLARE
result bigint;
sequencename character varying(50);
BEGIN
sequencename = CONCAT('constant_part_of_name_', _name_part);
IF((SELECT CAST(COUNT(*) AS INTEGER) FROM pg_class
WHERE relname LIKE sequencename) = 0)
THEN
CREATE SEQUENCE sequencename --here is the guy this is all about
MINVALUE 6000000
INCREMENT BY 1;
END IF;
SELECT nextval(sequencename) INTO result;
RETURN result;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
Now, let's say I want a sequence with _name_part = 'Whatever', so I type:
SELECT get_value('Whatever');
If sequence constant_part_of_name_Whatever does not exist, my function should create it and take a value; if it exists it should only take a value. However, I created sequence constant_part_of_name_sequencename.
How do I put the value of the variable in sequence definition to make it work?
The currently accepted answer has a number of problems. Most importantly it fails to take the schema into account.
Use instead:
CREATE OR REPLACE FUNCTION get_value(_name_part text)
RETURNS bigint AS
$func$
DECLARE
_seq text := 'constant_part_of_name_' || _name_part;
BEGIN
CASE (SELECT c.relkind = 'S'::"char"
FROM pg_namespace n
JOIN pg_class c ON c.relnamespace = n.oid
WHERE n.nspname = current_schema() -- or provide your schema!
AND c.relname = _seq)
WHEN TRUE THEN -- sequence exists
-- do nothing
WHEN FALSE THEN -- not a sequence
RAISE EXCEPTION '% is not a sequence!', _seq;
ELSE -- sequence does not exist, name is free
EXECUTE format('CREATE SEQUENCE %I MINVALUE 6000000 INCREMENT BY 1', _seq);
END CASE;
RETURN nextval(_seq);
END
$func$ LANGUAGE plpgsql;
SQL Fiddle.
Major points
Your test was needlessly expensive and incorrect. You need to take the schema into account. A sequence of the same name can exist in another schema, which would make your function fail.
I use the current schema as default, since you did not specify otherwise. Details:
How does the search_path influence identifier resolution and the "current schema"
You also need to be aware that the name of a sequence conflicts with other names of other objects in the same schema. Details:
How to create sequence if not exists
varchar(50) as data type is pointless and may cause problems if you enter a longer string. Just use text or varchar.
The assignment operator in plpgsql is :=, not =.
You can assign a variable at declaration time. Shorter, cheaper, cleaner.
You need dynamic SQL, I am using format() with %I to escape the identifier properly. Details:
INSERT with dynamic table name in trigger function
concat() is only useful if NULL values can be involved. I assume you don't want to pass NULL.
VOLATILE is default and therefore just noise.
If you want to return NULL on NULL input, add STRICT.
Try this. Hope this work for you.
CREATE OR REPLACE FUNCTION get_value(_name_part character varying) RETURNS INTEGER AS
$BODY$
DECLARE
result bigint;
sequencename character varying(50);
v_sql character varying;
BEGIN
sequencename = CONCAT('constant_part_of_name_', _name_part);
IF((SELECT CAST(COUNT(*) AS INTEGER) FROM pg_class WHERE relname LIKE sequencename) = 0)
THEN
v_sql := 'CREATE SEQUENCE '||sequencename||'
MINVALUE 6000000
INCREMENT BY 1;';
EXECUTE v_sql;
END IF;
SELECT nextval(sequencename) INTO result ;
RETURN result;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
CREATE OR REPLACE FUNCTION get_value(_name_part character varying) RETURNS INTEGER AS
$BODY$
DECLARE
result bigint;
sequencename character varying(50);
BEGIN
sequencename = CONCAT('constant_part_of_name_', _name_part);
IF (select exists(SELECT relname FROM pg_class c WHERE c.relkind = 'S' and relname = ''''||sequencename||'''') = false )
THEN
execute 'CREATE SEQUENCE '||sequencename||'MINVALUE 6000000 INCREMENT BY 1';
else
END IF;
execute 'SELECT nextval('''||sequencename||''')' INTO result;
RETURN result;
END;
$BODY$
LANGUAGE plpgsql VOLATILE

cannot pass dynamic query to sql-function

I cannot seem to find a way to pass my query as a parameter to my sql-function. My problem is table 'my_employees1' could be dynamic.
DROP FUNCTION function_test(text);
CREATE OR REPLACE FUNCTION function_test(text) RETURNS bigint AS '
DECLARE ret bigint;
BEGIN
SELECT count(mt.id) INTO ret
FROM mytable as mt
WHERE mt.location_id = 29671
--and mt.employee_id in (SELECT id from my_employees1);
--and mt.employee_id in ($1);
$1;
RETURN ret;
END;
' LANGUAGE plpgsql;
select function_test('and mt.employee_id in (SELECT id from my_employees1)');
select function_test('SELECT id from my_employees1');
It must be dynamically built:
DROP FUNCTION function_test(text);
CREATE OR REPLACE FUNCTION function_test(text) RETURNS bigint AS $$
DECLARE
ret bigint;
BEGIN
execute(format($q$
SELECT count(mt.id) INTO ret
FROM mytable as mt
WHERE mt.location_id = 29671
%s; $q$, $1)
);
RETURN ret;
END;
$$ LANGUAGE plpgsql;
The $$ and $q$ are dollar quotes. They can be nested as long as the inner identifier is different. In addition to the obvious advantages of permitting the use of unquoted quotes and being nestable it also let the syntax highlighting do its work.