Not able to execute query in function using input parameter? - postgresql

I have this function, which for some reason is causing an issue with thw input parameter i pass to its execute statement.
DROP FUNCTION IF EXISTS ingoing_outgoing_reference_intigrity_breach(altered_table_name text, entity_ids bigint[]);
DROP TYPE IF EXISTS detection_intgrity_result;
CREATE or REPLACE AGGREGATE range_merge(anyrange)
(
sfunc = range_merge,
stype = anyrange
);
create type detection_intgrity_result
as (source_entity_id bigint,
source_valid tsrange,
causes_intigrity_breach boolean,
depending_entity_internal_name text,
depending_entity_id bigint,
depending_valid tsrange
);
CREATE OR REPLACE FUNCTION ingoing_outgoing_reference_intigrity_breach(altered_table_name text, entity_ids bigint[])
RETURNS setof detection_intgrity_result
AS $$
DECLARE
i record;
result_row detection_intgrity_result;
BEGIN
FOR i IN (
SELECT
tc.table_name, kcu.column_name,
ccu.table_name AS foreign_table_name
FROM
information_schema.table_constraints AS tc
JOIN
information_schema.key_column_usage AS kcu
ON
tc.constraint_name = kcu.constraint_name
JOIN
information_schema.constraint_column_usage AS ccu
ON
ccu.constraint_name = tc.constraint_name
WHERE
constraint_type = 'FOREIGN KEY' and
ccu.column_name != 'id' and
kcu.column_name != 'entity_id' and
ccu.table_name = altered_table_name OR tc.table_name = altered_table_name
and ccu.table_name != tc.table_name) LOOP
EXECUTE format('SELECT '||i.table_name||'.entity_id, '||i.table_name||'.'||i.column_name||',
range_merge('||i.foreign_table_name||'_registration.valid) #> range_merge('||i.table_name||'.valid) as lifespan,
range_merge('||i.foreign_table_name||'_registration.valid) <# range_merge('||i.table_name||'.valid) as reverse_lifespan
from '||i.foreign_table_name||'_registration, '||i.table_name||'
where '||i.table_name||'.'||i.column_name||' in $2
and '||i.table_name||'_registration.registration #> now()::timestamp
GROUP BY '||i.table_name||'.'||i.column_name||', '||i.table_name||'_registration.entity_id') using entity_ids;
END LOOP;
END
$$ LANGUAGE plpgsql;
select * from ingoing_outgoing_reference_intigrity_breach('country', '{1,12,1}')
Am i not able to pass input parameters to a execute statement?
The function look up tables which have ingoing and outgoing refereneces and ensure that data between those two is still valid.

There are three problems with that EXECUTE statement:
You use parameter $2, which corresponds to the second argument, but there is only a single argument.
You are trying to pass an array as IN list, which won't work. Use the equivalent, but correct
WHERE colname = ANY ($1)
You construct an SQL string with ||, which makes the code vulnerable to SQL injection. Use the format function with the %I pattern to make the code safe and more readable.

Related

Using PERFORM in recursive query function in PostgreSQL

I have a recursive query which I want to use in PostgreSQL function, and it should return a Boolean value.
CREATE OR REPLACE FUNCTION store.is_item(object1 VARCHAR(40), object2 VARCHAR(40))
RETURNS BOOLEAN AS $$
BEGIN
WITH RECURSIVE externals AS (
SELECT object_id, used_id
FROM store.obj_depend
WHERE external = true
), history AS (
SELECT content_id AS id
FROM store.minfos
WHERE id= $2
UNION
SELECT externals.used_id
FROM externals
INNER JOIN history ON history.id = externals.object_id
),
PERFORM (SELECT c.id FROM store.cinfo AS c WHERE c.id = $1 INNER JOIN history
ON c.id = history.id);
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;
When I try this it gives asyntax error at or near SELECT error.
The PERFORM is plpgsql statement and cannot be used inside any SQL command.
You can use PERFORM like proposed #klin, but then CTE is used inside a subquery, and subquery returns one row every time. Then a FOUND variable should be every time true.
In this case is better to use auxiliary variable as targer:
CREATE OR REPLACE FUNCTION store.is_item(object1 VARCHAR(40), object2 VARCHAR(40))
RETURNS BOOLEAN AS $$
DECLARE r record;
BEGIN
WITH RECURSIVE
externals AS (SELECT object_id, used_id
FROM store.obj_depend
WHERE external = true)
history AS (SELECT content_id AS id
FROM store.minfos
WHERE id = object2
UNION
SELECT externals.used_id
FROM externals
INNER JOIN history ON history.id = externals.object_id)
SELECT c.id
FROM store.cinfo AS c
INNER JOIN history ON c.id = history.id
WHERE c.id = $1
INTO r;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;

PostgreSQL function to update a serial key column for a variable table

I have a system that syncs various tables between servers at various times. It works fine, except that some tables have SERIAL key columns that get out of date because the sync doesn't update the sequences. So I'm trying to write a pl/pgsql function to make sure that if a table (named in the parameter) has a serial key, its next value is one greater than the greatest value in the table.
Actually, I think I've finally done it, but I figured I'd post the question anyhow, so people can use it or suggest improvements.
You don't really need a function for that. With a variation of this answer this can be done with a single statement:
First we need to find all columns that have use a sequence as a default value:
select table_schema, table_name, column_name,
pg_get_serial_sequence(format('%I.%I', table_schema, table_name), column_name)
from information_schema.columns
where table_schema = 'public'
and column_default like 'nextval%'
Then we can calculate the max value for each of those columns using query_to_xml() and use that result to call setval() for each sequence.
with sequences as (
select table_schema, table_name, column_name,
pg_get_serial_sequence(format('%I.%I', table_schema, table_name), column_name) as col_sequence
from information_schema.columns
where table_schema = 'public' --<< adjust for your schemas
and column_default like 'nextval%'
), maxvals as (
select table_schema, table_name, column_name, col_sequence,
(xpath('/row/max/text()',
query_to_xml(format('select coalesce(max(%I),0) from %I.%I', column_name, table_schema, table_name), true, true, ''))
)[1]::text::bigint as max_val
from sequences
where col_sequence is not null
)
select table_schema,
table_name,
column_name,
col_sequence,
max_val,
setval(col_sequence, max_val)
from maxvals;
This appears to work. It seems one needs to use EXECUTE a lot.
-- Update the serial key sequence of this given table, if appropriate.
CREATE OR REPLACE FUNCTION update_serial(i_table CHARACTER VARYING)
RETURNS CHARACTER VARYING AS $$
DECLARE
v_key_col CHARACTER VARYING;
v_seq_name CHARACTER VARYING;
v_max RECORD;
BEGIN
-- Get the name of the primary key, if any.
SELECT c.column_name, c.data_type INTO v_key_col
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
JOIN information_schema.columns AS c
ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
WHERE constraint_type = 'PRIMARY KEY' and tc.table_name = i_table;
IF v_key_col IS NULL THEN RETURN 'No key found';
END IF;
-- Get the name of the sequence that determines the next number for the primary key, if any.
SELECT pg_get_serial_sequence(i_table, v_key_col) INTO v_seq_name;
IF v_seq_name IS NULL THEN RETURN 'No sequence found';
END IF;
-- Get the maximum value in the primary key data, and add 1.
EXECUTE 'SELECT MAX(' || v_key_col || ') + 1 m FROM ' || i_table INTO v_max;
-- Set the value of the sequence, converting to regclass and back to text so as to clean up the name and remove
-- the schema. It needs to put its output somewhere though we're not using it, so it goes back into v_max.
SELECT SETVAL(quote_ident(v_seq_name::regclass::text), v_max.m) INTO v_max;
RETURN 'Done';
END;
$$ LANGUAGE 'plpgsql';
COMMENT ON FUNCTION update_serial(i_table CHARACTER VARYING) IS
'Update the serial key sequence of this given table, if appropriate.';

LOOP for table function

I'm a newbie in Posgresql
I have a table function:
CREATE OR REPLACE FUNCTION stage.get_primary_key_info(
schemaName text,
tableName text
) RETURNS TABLE(constraint_name text, column_name text) AS
$BODY$
SELECT c.constraint_name, c.column_name
FROM information_schema.key_column_usage AS c
LEFT JOIN information_schema.table_constraints AS t
ON t.constraint_name = c.constraint_name
WHERE t.table_schema = schemaName
AND t.table_name = tableName
AND t.constraint_type = 'PRIMARY KEY'
;
$BODY$ LANGUAGE sql;
And I'm trying to use this function like:
FOR c IN (SELECT * FROM stage.get_primary_key_info(target_schema, stmt.tablename))
LOOP
joinFields = joinFields || FORMAT('t.%s = s.%s AND', c.column_name);
END LOOP;
But I have this error:
The loop variable of the tuples must be a variable of the type record
or tuple or a list of scalar variables
Try declaring c as a record. You are also missing a second parameter in the FORMAT string.
Also, take a look at string_agg and you might be able to skip that loop entirely. Here is an example (substituting y for the second parameter):
SELECT string_agg(FORMAT('t.%s = s.%s', column_name, 'y'), ' AND ')
FROM get_primary_key_info(target_schema, stmt.tablename)
;

POSTGRESQL- Query has no destination for result data

I'm new to postgres and to programming and I already searched for solution for this but I couldn't quite get it. I'm trying to make a function that will return information about all the customers on that particular country whenever I call the country. This is the error that pops up. I'm really sorry for asking this but I've been stuck here since yesterday.
ERROR: query has no destination for result data
HINT: If you want to discard the results of a SELECT, use PERFORM instead.
CONTEXT: PL/pgSQL function country(text) line 5 at SQL statement
Here is the function:
create or replace function country(text) returns text as $$
begin
select customer_id, customer.first_name, customer.last_name
from customer
inner join address on customer.address_id = address.address_id
inner join city on address.city_id = city.city_id
inner join country on city.country_id = country.country_id
where country = '$1';
end;
$$
language plpgsql;
If you are executing a select statement in a PL/pgSQL function, then you should place the result of the query in some variable(s) (= the destination). Then you work with the variable(s) in the function. You should also have a RETURN statement.
create or replace function country(text) returns text as $$
declare -- declare some variables
id integer;
fname text;
lname text;
begin
select customer_id, customer.first_name, customer.last_name
into id, fname, lname -- store query results in variables
from customer
inner join address on customer.address_id = address.address_id
inner join city on address.city_id = city.city_id
inner join country on city.country_id = country.country_id
where country = $1; -- don't quote parameter references
-- do something with the variables and return a value from the function
return format('%s: %s %s', id, upper(lname), fname);
end;
$$ language plpgsql;
Do note that the above only works if the query returns a single row. If the query returns multiple rows you can use a loop in the function. Even simpler, you can just return the results from the query like so:
create or replace function country(text)
returns table (id integer, first_name varchar, last_name varchar) as $$
begin
return query
select customer_id, customer.first_name, customer.last_name
from customer
inner join address on customer.address_id = address.address_id
inner join city on address.city_id = city.city_id
inner join country on city.country_id = country.country_id
where country = $1;
end;
$$ language plpgsql;
But like Evan Carroll said, unless you need a PL/pgSQL function to modify the data before returning it, you are better off with a simple view.
CREATE OR REPLACE FUNCTIOn func_process_client_contact(p_client_version_id bigint, p_publish_id bigint)
RETURNS TABLE(status_message character varying)
LANGUAGE plpgsql
AS $function$
declare
p_rowcount int;
BEGIN
-- validating input parameters : start
IF ( p_client_version_id IS NULL OR p_publish_id IS NULL )
THEN
RETURN;
END IF;
-- validating input parameters : end
WITH cte_target AS
(
SELECT
g.name AS clientname,
gcr.group_id AS clientid,
cr.id AS clientrelationid,
crc.id AS clientrelationcontactid,
crd.id AS clientrelationdesignation_id
FROM GROUPS g
JOIN group_client_relation gcr ON gcr.group_id =g.id
JOIN client_relation cr ON cr.id = gcr.client_relation_id
JOIN client_relation_contact crc ON crc.client_relation_id =cr.id
JOIN client_relation_designation crd ON cr.client_relation_designation_id =crd.id
)
SELECT * FROM cte_target ct WHERE ct.clientname='ABC';
GET DIAGNOSTICS p_rowcount = ROW_COUNT;
RAISE NOTICE 'returned % rows', p_rowcount;
IF ( p_rowcount=0 )
THEN
RETURN query SELECT 'hello' AS status_message;
ELSE
RETURN query SELECT 'Success' AS status_message;
END IF;
END
$function$
Please use below to get result of given function..
create or replace function country(in_parameter text,out out_res refcursor) as $$
begin
open out_res for
select customer_id, customer.first_name, customer.last_name
from customer
inner join address on customer.address_id = address.address_id
inner join city on address.city_id = city.city_id
inner join country on city.country_id = country.country_id
where country = '$1';
end;
$$
language plpgsql;
This is not normal for SQL. Normally, this would be a VIEW.
CREATE VIEW myView AS
SELECT customer_id, customer.first_name, customer.last_name
FROM customer
INNER JOIN address USING (address_id)
INNER JOIN city USING (city_id)
INNER JOIN country USING (country_id);
Then you do
SELECT * FROM myView WHERE country = ?
All of that said, if you insist on making this a function, and you shouldn't, you should make it a LANAGUAGE SQL and not LANGUAGE plppsql.
It worked for my colleague when she used OPEN MYCURS before the select query and RETURN MYCURS after the select query.
There might be some situations where we want to call another psql function from a function. If we just want to invoke the function and don't assign the return value to anything, then using select inner_function_call(); inside the main function will throw this error. As the hint suggests, use perform inner_function_call() instead.

EXECUTE...INTO...USING statement in PL/pgSQL can't execute into a record?

I'm attempting to write an area of a function in PL/pgSQL that loops through an hstore and sets a record's column(the key of the hstore) to a specific value (the value of the hstore). I'm using Postgres 9.1.
The hstore will look like: ' "column1"=>"value1","column2"=>"value2" '
Generally, here is what I want from a function that takes in an hstore and has a record with values to modify:
FOR my_key, my_value IN
SELECT key,
value
FROM EACH( in_hstore )
LOOP
EXECUTE 'SELECT $1'
INTO my_row.my_key
USING my_value;
END LOOP;
The error which I am getting with this code:
"myrow" has no field "my_key". I've been searching for quite a while now for a solution, but everything else I've tried to achieve the same result hasn't worked.
Simpler alternative to your posted answer. Should perform much better.
This function retrieves a row from a given table (in_table_name) and primary key value (in_row_pk), and inserts it as new row into the same table, with some values replaced (in_override_values). The new primary key value as per default is returned (pk_new).
CREATE OR REPLACE FUNCTION f_clone_row(in_table_name regclass
, in_row_pk int
, in_override_values hstore
, OUT pk_new int)
LANGUAGE plpgsql AS
$func$
DECLARE
_pk text; -- name of PK column
_cols text; -- list of names of other columns
BEGIN
-- Get name of PK column
SELECT INTO _pk a.attname
FROM pg_catalog.pg_index i
JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = i.indkey[0] -- single PK col!
WHERE i.indrelid = in_table_name
AND i.indisprimary;
-- Get list of columns excluding PK column
SELECT INTO _cols string_agg(quote_ident(attname), ',')
FROM pg_catalog.pg_attribute
WHERE attrelid = in_table_name -- regclass used as OID
AND attnum > 0 -- exclude system columns
AND attisdropped = FALSE -- exclude dropped columns
AND attname <> _pk; -- exclude PK column
-- INSERT cloned row with override values, returning new PK
EXECUTE format('
INSERT INTO %1$I (%2$s)
SELECT %2$s
FROM (SELECT (t #= $1).* FROM %1$I t WHERE %3$I = $2) x
RETURNING %3$I'
, in_table_name, _cols, _pk)
USING in_override_values, in_row_pk -- use override values directly
INTO pk_new; -- return new pk directly
END
$func$;
Call:
SELECT f_clone_row('tbl', 1, '"col1"=>"foo_new","col2"=>"bar_new"');
db<>fiddle here
Old sqlfiddle
Use regclass as input parameter type, so only valid table names can be used to begin with and SQL injection is ruled out. The function also fails earlier and more gracefully if you should provide an illegal table name.
Use an OUT parameter (pk_new) to simplify the syntax.
No need to figure out the next value for the primary key manually. It is inserted automatically and returned after the fact. That's not only simpler and faster, you also avoid wasted or out-of-order sequence numbers.
Use format() to simplify the assembly of the dynamic query string and make it less error-prone. Note how I use positional parameters for identifiers and unquoted strings respectively.
I build on your implicit assumption that allowed tables have a single primary key column of type integer with a column default. Typically serial columns.
Key element of the function is the final INSERT:
Merge override values with the existing row using the #= operator in a subselect and decompose the resulting row immediately.
Then you can select only relevant columns in the main SELECT.
Let Postgres assign the default value for the PK and get it back with the RETURNING clause.
Write the returned value into the OUT parameter directly.
All done in a single SQL command, that is generally fastest.
Since I didn't want to have to use any external functions for speed purposes, I created a solution using hstores to insert a record into a table:
CREATE OR REPLACE FUNCTION fn_clone_row(in_table_name character varying, in_row_pk integer, in_override_values hstore)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
my_table_pk_col_name varchar;
my_key text;
my_value text;
my_row record;
my_pk_default text;
my_pk_new integer;
my_pk_new_text text;
my_row_hstore hstore;
my_row_keys text[];
my_row_keys_list text;
my_row_values text[];
my_row_values_list text;
BEGIN
-- Get the next value of the pk column for the table.
SELECT ad.adsrc,
at.attname
INTO my_pk_default,
my_table_pk_col_name
FROM pg_attrdef ad
JOIN pg_attribute at
ON at.attnum = ad.adnum
AND at.attrelid = ad.adrelid
JOIN pg_class c
ON c.oid = at.attrelid
JOIN pg_constraint cn
ON cn.conrelid = c.oid
AND cn.contype = 'p'
AND cn.conkey[1] = at.attnum
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE c.relname = in_table_name
AND n.nspname = 'public';
-- Get the next value of the pk in a local variable
EXECUTE ' SELECT ' || my_pk_default
INTO my_pk_new;
-- Set the integer value back to text for the hstore
my_pk_new_text := my_pk_new::text;
-- Add the next value statement to the hstore of changes to make.
in_override_values := in_override_values || hstore( my_table_pk_col_name, my_pk_new_text );
-- Copy over only the given row to the record.
EXECUTE ' SELECT * '
' FROM ' || quote_ident( in_table_name ) ||
' WHERE ' || quote_ident( my_table_pk_col_name ) ||
' = ' || quote_nullable( in_row_pk )
INTO my_row;
-- Replace the values that need to be changed in the column name array
my_row := my_row #= in_override_values;
-- Create an hstore of my record
my_row_hstore := hstore( my_row );
-- Create a string of comma-delimited, quote-enclosed column names
my_row_keys := akeys( my_row_hstore );
SELECT array_to_string( array_agg( quote_ident( x.colname ) ), ',' )
INTO my_row_keys_list
FROM ( SELECT unnest( my_row_keys ) AS colname ) x;
-- Create a string of comma-delimited, quote-enclosed column values
my_row_values := avals( my_row_hstore );
SELECT array_to_string( array_agg( quote_nullable( x.value ) ), ',' )
INTO my_row_values_list
FROM ( SELECT unnest( my_row_values ) AS value ) x;
-- Insert the values into the columns of a new row
EXECUTE 'INSERT INTO ' || in_table_name || '(' || my_row_keys_list || ')'
' VALUES (' || my_row_values_list || ')';
RETURN my_pk_new;
END
$function$;
It's quite a bit longer than what I had envisioned, but it works and is actually quite speedy.