Syntax error in dynamic SQL in pl/pgsql function - sql

I am using pl/pgsql in PostgreSQL 10, to create complex queries. I am testing a query with a couple of JOINs and ANDs. This is what I have so far:
DROP FUNCTION IF EXISTS search_person(name text);
CREATE FUNCTION search_person(name text) RETURNS TABLE(address_id integer, address_geom text, event_name text) AS $$
--DECLARE
BEGIN
RETURN QUERY EXECUTE
'SELECT address.id, event.name, address.geom
FROM event JOIN person JOIN address JOIN person_address JOIN event_person
WHERE
person_address.event_id = event.id AND
event_person.event_id = event.id AND
person.id = event_person.person_id AND
person.name like
$1'
USING name;
END;
$$
LANGUAGE plpgsql;
I get no errors while creating this function. I call it like so select search_person('nick'); and I get:
ERROR: syntax error at or near "WHERE"
LINE 3: WHERE
^
QUERY: SELECT address.id, event.name, address.geom
FROM event JOIN person JOIN address JOIN person_address JOIN event_person
WHERE
person_address.event_id = event.id AND
event_person.event_id = event.id AND
person.id = event_person.person_id AND
person.name like
$1
CONTEXT: PL/pgSQL function search_creator(text) line 5 at RETURN QUERY
SQL state: 42601
I cannot see or fix the problem. I tried replacing AND with || in the WHERE clause, but nothing changed.
What should I do?
EDIT
This is the code I have now and I get an empty table, even though I should get results, according to my database data that I checked.
CREATE FUNCTION search_person(name character(600)) RETURNS TABLE(address_id bigint, address_geom geometry, event_name character(200)) AS $$
BEGIN
RETURN QUERY EXECUTE
'SELECT address.id, address.geom, event.name
FROM
person
JOIN event_creator ON event_person.person_id = person.id
JOIN event ON event.id = event_person.event_id
JOIN person_address ON person_address.event_id = event.id
JOIN address ON address.id = cep.address_id
WHERE person.name LIKE $1'
USING name;
END;
$$
LANGUAGE plpgsql;

When creating a PL/pgSQL function, the function body is saved as string literal as is. Only superficial syntax checks are applied. Contained statements are not actually executed or tested on a deeper level.
However, basic syntax errors like you have in your query string would still be detected in actual SQL statements. But you are using dynamic SQL with EXECUTE. The statement is contained in a nested string literal and is your responsibility alone.
This seems to be misguided to begin with. There is no apparent reason for dynamic SQL. (Unless you have very uneven data distribution and want to force Postgres to generate a custom plan for each input value.)
If you had used a plain SQL statement, you would have gotten the error message at creation time:
CREATE OR REPLACE FUNCTION search_person(name text) -- still incorrect!
RETURNS TABLE(address_id integer, address_geom text, event_name text) AS
$func$
BEGIN
RETURN QUERY
SELECT address.id, event.name, address.geom
FROM event JOIN person JOIN address JOIN person_address JOIN event_person
WHERE
person_address.event_id = event.id AND
event_person.event_id = event.id AND
person.id = event_person.person_id AND
person.name like $1; -- still $1, but refers to func param now!
END
$func$ LANGUAGE plpgsql;
The SQL statement is still invalid. [INNER] JOIN requires a join condition - like Nick commented. And I don't see the need for PL/pgSQL at all. A simple SQL function should serve well:
CREATE FUNCTION search_person(name text)
RETURNS TABLE(address_id integer, address_geom text, event_name text) AS
$func$
SELECT a.id, a.geom, e.name -- also fixed column order to match return type
FROM person AS p
JOIN event_person AS ep ON ep.person_id = p.id
JOIN event AS e ON e.id = ep.event_id
JOIN person_address AS pa ON pa.event_id = e.id
JOIN address AS a ON a.id = pa.address_id -- missing join condition !!
WHERE p.name LIKE $1;
$func$ LANGUAGE sql;
I rewrote the query to fix syntax error, using table aliases for better readability. Finally, I also added one more missing condition based on an educated guess: a.id = pa.address_id.
Now it should work.
Related:
plpgsql function not inserting data as intended
Difference between language sql and language plpgsql in PostgreSQL functions
Or no function at all, just use a prepared statement instead. Example:
Split given string and prepare case statement
If you need dynamic SQL after all, pass values with the USING clause like you had it and make sure to defend against SQL injection when concatenating queries. Postgres provides various tools:
SQL injection in Postgres functions vs prepared queries
Define table and column names as arguments in a plpgsql function?
Table name as a PostgreSQL function parameter

Related

Column doesn't exist when using WITH statement PostgreSQL

I want to create a function to be used to get the node traversal path.
CREATE TYPE IDType AS (id uuid);
drop function F_ItemPath;
CREATE OR REPLACE FUNCTION F_ItemPath (item record)
RETURNS TABLE (item_id uuid, depth numeric)
AS $$
BEGIN
return QUERY
WITH recursive item_path AS (
SELECT ic.parent_item_id, depth=1
from item_combination ic, item i
WHERE ic.child_item_id=i.id
UNION all
SELECT ic.parent_item_id, depth=ip.depth + 1
FROM item_path ip, item_combination ic WHERE ip.parent_item_id=ic.child_item_id
)
SELECT item_id=ip.parent_item_id, depth=ip.depth FROM item_path ip;
END; $$
LANGUAGE plpgsql;
select * from F_ItemPath(('55D6F516-7D8F-4DF3-A4E5-1E3F505837A1', 'FFE2A4D3-267C-465F-B4B4-C7BB2582F1BC'))
there has two problems:
I tried using user-defined type to set parameter type CREATE TYPE IDType AS (id uuid);, but I don't know how to call the function with table argument
there has an error that says:
SQL Error [42703]: ERROR: column ip.depth does not exist
Where: PL/pgSQL function f_itempath(record) line 3 at RETURN QUERY
what I expected is I can use the function normally and the argument can be supplied from other tables.
this is the full query that you can try:
http://sqlfiddle.com/#!15/9caba/1
I made the query in DBEAVER app, it will have some different error message.
I suggest you can experiment with it outside sqlfiddle.
The expression depth=1 tests if the column depth equals the value 1 and returns a boolean value. But you never give that boolean expression a proper name.
Additionally you can't add numbers to boolean values, so the expression depth=ip.depth + 1 tries to add 1 to a value of true or false - which fails obviously. If it did work, it would then compare that value with the value in the column depth again.
Did you intend to alias the value 1 with the name depth? Then you need to use 1 as depth and ip.depth + 1 as depth in the recursive part.
In the final select you have the same error - using boolean expressions instead of a column alias
It's also highly recommended to use explicit JOIN operators which were introduced in the SQL standard over 30 years ago.
Using PL/pgSQL to wrap a SQL query is also a bit of an overkill. A SQL function is enough.
Using an untyped record as a parameter seems highly dubious. It won't allow you to access columns using e.g. item.id. But given your example call, it seems you simply want to pass multiple IDs for the anchor (no-recursive) part of the query. That's better done using an array or a varadic parameter which allows listing multiple parameters with commas.
So you probably want something like this:
drop function f_itempath;
CREATE OR REPLACE FUNCTION f_itempath(variadic p_root_id uuid[])
RETURNS TABLE (item_id uuid, depth integer)
as
$$
WITH recursive item_path AS (
SELECT ic.parent_item_id, 1 as depth
FROM item_combination ic
WHERE ic.child_item_id = any(p_root_id) --<< no join needed to access the parameter
UNION all
SELECT ic.parent_item_id, ip.depth + 1
FROM item_path ip
JOIN item_combination ic ON ip.parent_item_id = ic.child_item_id
)
SELECT ip.parent_item_id as item_id, ip.depth
FROM item_path ip;
$$
language sql
stable;
Then you can call it like this (note: no parentheses around the parameters)
select *
from f_itempath('55d6f516-7d8f-4df3-a4e5-1e3f505837a1', 'ffe2a4d3-267c-465f-b4b4-c7bb2582f1bc');
select *
from f_itempath('55d6f516-7d8f-4df3-a4e5-1e3f505837a1', 'ffe2a4d3-267c-465f-b4b4-c7bb2582f1bc', 'df366232-f200-4254-bad5-94e11ea35379');
select *
from f_itempath('55d6f516-7d8f-4df3-a4e5-1e3f505837a1');

Postgres declaration type for multiple rows

I have a postgres function that uses a %ROWTYPE declaration. When I execute this function I get a failure about returning more than one row "code":"P0003","message":"query returned more than one row"
I can successfully execute the query without the returning * into assignments; statement. Which leads me to believe the assignments data.assignment%ROWTYPE; is only for one row?
create or replace function assign(order_id int) returns json as $$
declare
assignments data.assignment%ROWTYPE;
begin
insert into data.assignment
(order_item_id, pharmacy_id)
(
select oi.id as order_item_id, mci.pharmacy_id
from api.order_items as oi
inner join api.min_cost_inventory_items as mci on mci.drug_id = oi.drug_id
where oi.order_id = $1
)
returning * into assignments;
return json_build_object(
'assignments', assignments
);
end
$$ security definer language plpgsql;
revoke all privileges on function assign(int) from public;
Yes. The %ROWTYPE is not strictly necessary, but in any case it holds only one row. You could make an array of data.assignment[], but then you need to loop to fill it.
You can simplify the function as follows:
create or replace function assign(_order_id int) returns jsonb as $$
with i as (
insert into data.assignment
(order_item_id, pharmacy_id)
select oi.id as order_item_id, mci.pharmacy_id
from api.order_items as oi
inner join api.min_cost_inventory_items as mci
on mci.drug_id = oi.drug_id
where oi.order_id = _order_id
returning *
)
select jsonb_agg(to_jsonb(i)) from i;
$$ security definer language sql;

Can I transfer SQL Query into a FUNCTION?

Currently, I have some SQL queries which looks like this:
Drop Table X;
Create Table X(id INTEGER);
Insert Into X
select ..
from..
where a.name = GIVENNAME;
Select SUM(..)
from ..
..
order by date desc;
And I want to put all these into a SQL Function, where I can choose the Parameter "GIVENNAME" when I call the function.
Is there a way to make this possible?
I would know how to do it in JSON/Java, but I have really no clue how to make it as a Function in SQL (using Oracle).
Edit:
After pointing out some things, I want to add my current code:
DROP TABLE TEMPTABLE;
CREATE TABLE TEMPTABLE
(mitID INTEGER);
INSERT INTO TEMPTABLE
select m.mitid
from mitarbeiter m
inner join abteilungen a on m.abt = a.abtid
where a.abtname = #GIVENNAME;
select SUM(g.kosten)
from gehaelter g
left outer join gehaelter k
on g.mitarbeiter = k.mitarbeiter
and g.vondatum < k.vondatum
where k.mitarbeiter is null AND g.mitarbeiter in (select * from TEMPTABLE)
order by g.vondatum desc;
I'm currently more interested in a working solution than a nice & clean one
Fortunately you can have both:
create or replace function get_sum_kosten
( p_givenname in abteilungen.abtname%type )
return number
as
return_value number;
begin
select SUM(g.kosten)
into return_value
from gehaelter g
left outer join gehaelter k
on g.mitarbeiter = k.mitarbeiter
and g.vondatum < k.vondatum
where k.mitarbeiter is null
AND g.mitarbeiter in (select m.mitid
from mitarbeiter m
inner join abteilungen a on m.abt = a.abtid
where a.abtname = P_GIVENNAME
)
;
return return_value;
end;
Possible? Yes. Recommended? No.
For any DDL, you'd have to use dynamic SQL (EXECUTE IMMEDIATE). If queries are complex, those commands will be difficult to maintain.
INSERT is a DML, but you can't use it in a function, unless it is an autonomous transaction (and you'll have to commit (or rollback) within the function).
If it were a procedure, you'd - at least - avoid the last problem I mentioned. If you're returning something, use an OUT parameter.
Can't you use a (global) temporary table, instead? Create it once, use it many times. I understand that your code might be very complex and maybe it really can't fit into a single SELECT statement, but you should - at least - try to do that job in an Oracle spirit (i.e. it is not MS SQL Server).
example of procedure
https://www.sitepoint.com/stored-procedures-mysql-php/
like this
DELIMITER $$
CREATE PROCEDURE `avg_sal`(out avg_sal decimal)
BEGIN
select avg(sal) into avg_sal from salary;
END

Left join with dynamic table name derived from column

I am new in PostgreSQL and I wonder if it's possible to use number from table tbc as part of the table name in left join 'pa' || number. So for example if number is 456887 I want left join with table pa456887. Something like this:
SELECT tdc.cpa, substring(tdc.ku,'[0-9]+') AS number, paTab.vym
FROM public."table_data_C" AS tdc
LEFT JOIN concat('pa' || number) AS paTab ON (paTab.cpa = tdc.cpa)
And I want to use only PostgreSQL, not additional code in PHP for example.
Either way, you need dynamic SQL.
Table name as given parameter
CREATE OR REPLACE FUNCTION foo(_number int)
RETURNS TABLE (cpa int, nr text, vym text) AS -- adapt to actual data types!
$func$
BEGIN
RETURN QUERY EXECUTE format(
'SELECT t.cpa, substring(t.ku,'[0-9]+'), p.vym
FROM public."table_data_C" t
LEFT JOIN %s p USING (cpa)'
, 'pa' || _number
);
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM foo(456887)
Generally, you would sanitize table names with format ( %I ) to avoid SQL injection. With just an integer as dynamic input that's not necessary. More details and links in this related answer:
INSERT with dynamic table name in trigger function
Data model
There may be good reasons for the data model. Like partitioning / sharding or separate privileges ...
If you don't have such a good reason, consider consolidating multiple tables with identical schema into one and add the number as column. Then you don't need dynamic SQL.
Consider inheritance. Then you can add a condition on tableoid to only retrieve rows from a given child table:
SELECT * FROM parent_table
WHERE tableoid = 'pa456887'::regclass
Be aware of limitations for inheritance, though. Related answers:
Get the name of a row's source table when querying the parent it inherits from
Select (retrieve) all records from multiple schemas using Postgres
Name of 2nd table depending on value in 1st table
Deriving the name of the join table from values in the first table dynamically complicates things.
For only a few tables
LEFT JOIN each on tableoid. There is only one match per row, so use COALESCE.
SELECT t.*, t.tbl, COALESCE(p1.vym, p2.vym, p3.vym) AS vym
FROM (
SELECT cpa, ('pa' || substring(ku,'[0-9]+'))::regclass AS tbl
FROM public."table_data_C"
-- WHERE <some condition>
) t
LEFT JOIN pa456887 p1 ON p1.cpa = t.cpa AND p1.tableoid = t.tbl
LEFT JOIN pa456888 p2 ON p2.cpa = t.cpa AND p2.tableoid = t.tbl
LEFT JOIN pa456889 p3 ON p3.cpa = t.cpa AND p3.tableoid = t.tbl
For many tables
Combine a loop with dynamic queries:
CREATE OR REPLACE FUNCTION foo(_number int)
RETURNS TABLE (cpa int, nr text, vym text) AS
$func$
DECLARE
_nr text;
BEGIN
FOR _nr IN
SELECT DISTINCT substring(ku,'[0-9]+')
FROM public."table_data_C"
LOOP
RETURN QUERY EXECUTE format(
'SELECT t.cpa, _nr, p.vym
FROM public."table_data_C" t
LEFT JOIN %I p USING (cpa)
WHERE t.ku LIKE (_nr || '%')'
, 'pa' || _nr
);
END LOOP;
END
$func$ LANGUAGE plpgsql;

Concatenate multiple fields in query string in plpgsql

I am using plpgsql and hibernate and want to create a function which contains the query string given below. In the select clause I want to concatenate 3 fields but while running this query I am getting error message like:
ERROR: syntax error at or near "' '"
SQL state: 42601
Context: PL/pgSQL function "est_fn_dept_wise_emp_report" line 30 at open
I am new using stored functions, it might be a basic question but somehow I was unable to find a solution.
query1 = 'SELECT est_emp_empmaster.emp_no AS est_emp_empmaster_emp_no,
adm_m_department.dept_name AS adm_m_department_dept_name,
adm_m_subdepartment.sub_dept_id AS adm_m_subdepartment_sub_dept_id,
adm_m_subdepartment.sub_dept_name AS adm_m_subdepartment_sub_dept_name,
est_m_designation.desig_name AS est_m_designation_desig_name,
est_emp_empmaster.first_name'|| ' ' ||'est_emp_empmaster.middle_name'|| ' ' ||'est_emp_empmaster.surname AS empname
FROM public.adm_m_department adm_m_department
INNER JOIN public.adm_m_subdepartment adm_m_subdepartment
ON adm_m_department.dept_id = adm_m_subdepartment.dept_id
INNER JOIN public.est_emp_empmaster est_emp_empmaster
ON adm_m_department.dept_id = est_emp_empmaster.dept_id
AND adm_m_subdepartment.sub_dept_id = est_emp_empmaster.sub_dept_id
INNER JOIN public.est_emp_salary est_emp_salary
ON est_emp_empmaster.emp_no = est_emp_salary.emp_no
INNER JOIN public.est_m_designation est_m_designation
ON est_emp_salary.pre_desig_code = est_m_designation.desig_code
AND est_emp_salary.retired_flag ='|| quote_literal('N') ||'
WHERE est_emp_empmaster.corp_coun_id=0 or est_emp_empmaster.corp_coun_id is null or est_emp_empmaster.corp_coun_id = '|| quote_literal($1) ||'
ORDER BY adm_m_department.dept_id,adm_m_subdepartment.sub_dept_id,est_emp_empmaster.emp_no ASC';
OPEN refcur FOR
EXECUTE query1;
LOOP
FETCH refcur INTO return_record;
EXIT WHEN NOT FOUND;
RETURN NEXT return_record;
END LOOP;
CLOSE refcur;**
The above query runs fine if I execute it without executing through query string. But as I want to use this query for multiple conditions and in those condition I want to modify this query to get different results.
This can be much simpler, safer and faster (assuming at least Postgres 8.4):
CREATE OR REPLACE FUNCTION foo(_corp_coun_id int) -- guessing type
RETURNS TABLE (
emp_no int -- guessing data types ..
,dept_name text -- .. replace with actual types
,sub_dept_id int
,sub_dept_name text
,desig_name text
,empname text) AS
$func$
BEGIN
RETURN QUERY
SELECT em.emp_no
,dp.dept_name
,sb.sub_dept_id
,sb.sub_dept_name
,ds.desig_name
,concat_ws(' ', em.first_name, em.middle_name, em.surname) -- AS empname
FROM adm_m_department dp
JOIN adm_m_subdepartment su ON sb.dept_id = dp.dept_id
JOIN est_emp_empmaster em ON em.dept_id = sb.dept_id
AND em.sub_dept_id = sb.sub_dept_id
JOIN est_emp_salary sl ON sl.emp_no = em.emp_no
AND sl.retired_flag = 'N' -- untangled join cond.
JOIN est_m_designation ds ON ds.desig_code = sl.pre_desig_code
WHERE em.corp_coun_id = 0 OR
em.corp_coun_id IS NULL OR
em.corp_coun_id = $1
ORDER BY dp.dept_id, sb.sub_dept_id, em.emp_no;
END
$func$
LANGUAGE plpgsql SET search_path=public;
To address your primary question: use concat_ws() for simple and secure concatenation of multiple columns (doesn't fail with NULL).
You do not need dynamic SQL here since the variables are only values (not identifiers).
You do not need a CURSOR here.
You do not need a LOOP. RETURN QUERY does the same, simpler and faster.
You do not need column aliases, only the names of the OUT parameters (implicitly the column names in RETURNS TABLE (...)) are relevant.
Replace the multiple schema qualification public. in your query with a single SET search_path = public.
I also untangled your query, used short table aliases and reformatted to make it easier to read.
You don't even need plpgsql at all here. Can be a simpler SQL function:
CREATE OR REPLACE FUNCTION foo(_corp_coun_id int)
RETURNS TABLE (
emp_no int -- guessing data types ..
,dept_name text -- .. replace with actual types!
,sub_dept_id int
,sub_dept_name text
,desig_name text
,empname text) AS
$func$
SELECT em.emp_no
,dp.dept_name
,sb.sub_dept_id
,sb.sub_dept_name
,ds.desig_name
,concat_ws(' ', em.first_name, em.middle_name, em.surname) -- AS empname
FROM adm_m_department dp
JOIN adm_m_subdepartment sb ON sb.dept_id = dp.dept_id
JOIN est_emp_empmaster em ON em.dept_id = sb.dept_id
AND em.sub_dept_id = sb.sub_dept_id
JOIN est_emp_salary sl ON sl.emp_no = em.emp_no
AND sl.retired_flag = 'N' -- untangled join cond.
JOIN est_m_designation ds ON ds.desig_code = sl.pre_desig_code
WHERE em.corp_coun_id = 0 OR
em.corp_coun_id IS NULL OR
em.corp_coun_id = $1
ORDER BY dp.dept_id, sb.sub_dept_id, em.emp_no;
$func$
LANGUAGE sql SET search_path=public;
I found a solution to the above problem,
actually it was well working in normal query but i got problem while running it in dynamic query.
The solution to the above problem is as follows.Thanks again.. :)
est_emp_empmaster.first_name||'' ''||est_emp_empmaster.middle_name||'' ''||est_emp_empmaster.surname AS empname