pl/pgSQL - Proper data for parameter used in "WHERE IN" clause? - postgresql

I am attempting to create a PL/pgSQL function that accepts an array/list of values (properties) that will be used for filtering records using an "IN" clause.
CREATE OR REPLACE FUNCTION ticket.property_report(start_date DATE, end_date DATE, properties VARCHAR[]) RETURNS
TABLE(total_labor_mins numeric(10,2), total_parts_cost numeric(10,2), ticket_count bigint, property VARCHAR(6)) AS $$
BEGIN
RETURN QUERY SELECT SUM(c.total_labor_mins), SUM(c.total_parts_cost), COUNT(*) as ticket_count, t.property_id as property
FROM ticket.ticket AS t LEFT JOIN ticket.ticket_cost_row AS c ON t.id = c.ticket_id
WHERE t.property_id IN (properties) AND t.open_date >= start_date AND t.open_date <= end_date
GROUP BY t.property_id;
END;
$$ LANGUAGE plpgsql;
However, I am struggling with what type of data type to use in the parameter list. A single "varchar" is not appropriate as there could be many property values. However, whenever I use an array (as in the code above), I received the following message:
SELECT ticket.property_report(TO_DATE('2019-01-01', 'yyyy-MM-dd'), TO_DATE('2019-12-31', 'yyyy-MM-dd'), '{"5305"}');
ERROR: operator does not exist: character varying = character varying[]
LINE 3: WHERE t.property_id IN (properties) AND t.open_date >=...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
QUERY: SELECT SUM(c.total_labor_mins), SUM(c.total_parts_cost), COUNT(*) as ticket_count, t.property_id as property
FROM ticket.ticket AS t LEFT JOIN ticket.ticket_cost_row AS c ON t.id = c.ticket_id
WHERE t.property_id IN (properties) AND t.open_date >= start_date AND t.open_date <= end_date
GROUP BY t.property_id
CONTEXT: PL/pgSQL function ticket.property_report(date,date,character varying[]) line 3 at RETURN QUERY
What is the property data type to use for this purpose. In example, something that would translate to WHERE t.property_id IN ('123','12','1',...)?
Thanks.

The array is the correct data type, you just need to use an operator that works with that:
WHERE t.property_id = ANY (properties)

Related

How to use text input as column name(s) in a Postgres function?

I'm working with Postgres and PostGIS. Trying to write a function that that selects specific columns according to the given argument.
I'm using a WITH statement to create the result table before converting it to bytea to return.
The part I need help with is the $4 part. I tried it is demonstrated below and $4::text and both give me back the text value of the input and not the column value in the table if cols=name so I get back from the query name and not the actual names in the table. I also try data($4) and got type error.
The code is like this:
CREATE OR REPLACE FUNCTION select_by_txt(z integer,x integer,y integer, cols text)
RETURNS bytea
LANGUAGE 'plpgsql'
AS $BODY$
declare
res bytea;
begin
WITH bounds AS (
SELECT ST_TileEnvelope(z, x, y) AS geom
),
mvtgeom AS (
SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, $4
FROM table1 t, bounds
WHERE ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
)
SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
INTO res
FROM mvtgeom;
RETURN res;
end;
$BODY$;
Example for calling the function:
select_by_txt(10,32,33,"col1,col2")
The argument cols can be multiple column names from 1 and not limited from above. The names of the columns inside cols will be checked before calling the function that they are valid columns.
Passing multiple column names as concatenated string for dynamic execution urgently requires decontamination. I suggest a VARIADIC function parameter instead, with properly quoted identifiers (using quote_ident() in this case):
CREATE OR REPLACE FUNCTION select_by_txt(z int, x int, y int, VARIADIC cols text[] = NULL, OUT res text)
LANGUAGE plpgsql AS
$func$
BEGIN
EXECUTE format(
$$
SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
FROM (
SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom%s
FROM table1 t
JOIN (SELECT ST_TileEnvelope($1, $2, $3)) AS bounds(geom)
ON ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
) mvtgeom
$$, (SELECT ', ' || string_agg(quote_ident (col), ', ') FROM unnest(cols) col)
)
INTO res
USING z, x, y;
END
$func$;
db<>fiddle here
The format specifier %I for format() deals with a single identifier. You have to put in more work for multiple identifiers, especially for a variable number of 0-n identifiers. This implementation quotes every single column name, and only add a , if any column names have been passed. So it works for every possible input, even no input at all. Note VARIADIC cols text[] = NULL as last input parameter with NULL as default value:
Optional argument in PL/pgSQL function
Related:
quote_ident() does not add quotes to column name "first"
Column names are case sensitive in this context!
Call for your example (important!):
SELECT select_by_txt(10,32,33,'col1', 'col2');
Alternative syntax:
SELECT select_by_txt(10,32,33, VARIADIC '{col1,col2}');
More revealing call, with a third column name and malicious (though futile) intent:
SELECT select_by_txt(10,32,33,'col1', 'col2', $$col3'); DROP TABLE table1;--$$);
About that odd third column name and SQL injection:
https://www.explainxkcd.com/wiki/index.php/Little_Bobby_Tables
About VAIRADIC parameters:
Return rows matching elements of input array in plpgsql function
Pass multiple values in single parameter
Using an OUT parameter for simplicity. That's totally optional. See:
Returning from a function with OUT parameter
What I would not do
If you really, really trust the input to be a properly formatted list of 1 or more valid column names at all times - and you asserted that ...
the names of the columns inside cols will be checked before calling the function that they are valid columns
You could simplify:
CREATE OR REPLACE FUNCTION select_by_txt(z int, x int, y int, cols text, OUT res text)
LANGUAGE plpgsql AS
$func$
BEGIN
EXECUTE format(
$$
SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
FROM (
SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, %s
FROM table1 t
JOIN (SELECT ST_TileEnvelope($1, $2, $3)) AS bounds(geom)
ON ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
) mvtgeom
$$, cols
)
INTO res
USING z, x, y;
END
$func$;
(How can you be so sure that the input will always be reliable?)
You would need to use a dynamic query:
CREATE OR REPLACE FUNCTION select_by_txt(z integer,x integer,y integer, cols text)
RETURNS bytea
LANGUAGE 'plpgsql'
AS $BODY$
declare
res bytea;
begin
EXECUTE format('
WITH bounds AS (
SELECT ST_TileEnvelope($1, $2, $3) AS geom
),
mvtgeom AS (
SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, %I
FROM table1 t, bounds
WHERE ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
)
SELECT ST_AsMVT(mvtgeom, ''public.select_by_txt'')
FROM mvtgeom', cols)
INTO res
USING z,x,y;
RETURN res;
end;
$BODY$;

CREATE TABLE AS is not allowed in a non-volatile function in Postgresql

I have wrote this method on postgresql 10 :
create or replace function get_users()
returns TABLE(user_idd uuid, device_idd text, shapee text , datee timestamp) AS
$$
begin
create temp table lines as
SELECT DISTINCT user_id, device_id from olocations;
select uuid_generate_v4(),
o1.user_id,
o1.device_id,
st_astext(ST_Collect(o1.shape)),
date(o1.creation_date_time) as date
from olocations o1
inner join lines on o1. device_id = lines.device_id and o1.user_id = lines.user_id
where o1.user_id = 'd0edfc59-9923-44c3-9c34-ef5aad3cb810'
and o1.device_id = '89984320001811791540'
group by o1.user_id, o1.device_id, date
order by date ASC ;
DROP TABLE lines;
end
$$
LANGUAGE 'plpgsql'
IMMUTABLE
SECURITY DEFINER
COST 100;
After create method without any problem, when i call my method:
select * from get_users();
I got this error:
sql> select from get_users()
[2018-09-30 17:23:23] [0A000] ERROR: CREATE TABLE AS is not allowed in a non-volatile function
[2018-09-30 17:23:23] Where: SQL statement "create temp table lines as
[2018-09-30 17:23:23] SELECT DISTINCT user_id, device_id from olocations"
[2018-09-30 17:23:23] PL/pgSQL function get_users() line 3 at SQL statement
I think i can not create table in method? right?
The function cannot be IMMUTABLE, define it as VOLATILE.
Per the documentation:
Any function with side-effects must be labeled VOLATILE, so that calls to it cannot be optimized away.
In this case this side-effect is the table creation.
Update.
Use return query to return rows generated by the query:
...
return query
select uuid_generate_v4(),
o1.user_id,
o1.device_id,
st_astext(ST_Collect(o1.shape)),
date(o1.creation_date_time) as date
...

Postgres: check array of text has elements castable to domain type in the where clause of a select statement

Given I have defined domains with check constraints and I have the following query:
SELECT pm.name, ps.name, pm.id, ps.id
FROM
pylon_modules pm
INNER JOIN
pylon_symbols ps ON pm.id = ps.module_id
WHERE pm.name in ('Test','test') and ps.name in ('bang');
What is the simplest way to cast the symbols used in the where clause ? I need to assert they match a domain type.
Above 'Test' is not a valid module name.
I can set up a stored procedure but the query is so simple I'd prefer to prepare it.
If I use a stored procedure do I have to create an array version of my domain types ?
There is this, but it requires the cast target is of array type.
The following does the trick, your domains constraint violations will trigger errors:
CREATE OR REPLACE FUNCTION resolve_symbols(modules TEXT[], symbols TEXT[])
RETURNS TABLE(
moduleName VARCHAR(16),
symbolName VARCHAR(16),
moduleId INT,
symbolId INT
) AS $body$
SELECT pm.name, ps.name, pm.id , ps.id
FROM pylon_modules pm
INNER JOIN pylon_symbols ps ON pm.id = ps.module_id
WHERE pm.name IN (SELECT unnest(modules)::public.module_name) AND
ps.name IN (SELECT unnest(symbols)::public.element_name)
$body$ LANGUAGE sql;

How to pass timestamps to a function and retrieve resulting rows

I have created custom data type. In that I have given alias name of the one field. you will get that in body of the function below.
create type voucher as (
ori numeric, RECEIPT_NO numeric
, receipt_date timestamp with time zone, reg_no character varying
, patient_name character varying, tot_refund_bill_amount double precision
, username character varying );
Thea above statement completes successfully.
Then I want to create a function:
create or replace function billing.voucher_receipt (in_from_date timestamp with time zone, in_to_date timestamp with time zone)
returns setof voucher as $$
declare
out_put voucher%rowtype;
begin
return query(select C.receipt_no as ori ,A.RECEIPT_NO, receipt_date , A.reg_no, patient_name, tot_refund_bill_amount, username
from billing.tran_counter_receipt as a inner join mas_user as b on a.ent_by=b.uid AND cash_book='REFUND'
INNER JOIN billing.tran_BILL AS C ON C.REG_NO=A.REG_NO AND C.CASH_BOOK='GCASH' where receipt_date>=in_from_date and receipt_date<=in_to_date);
end;$$
LANGUAGE plpgsql
Executes without problem.
But when I call it with input like this:
select * from voucher_receipt ('2014-09-25 11:42:44.298346+05:30'
, '2014-09-29 11:03:47.573049+05:30')
it shows an error:
ERROR: function voucher_receipt(unknown, unknown) does not exist
LINE 1: select * from voucher_receipt ('2014-09-25 11:42:44.298346+0...
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
Can any one help me out from this?
Explain error
You created your function in the schema billing with:
create or replace function billing.voucher_receipt( ...
Then you call without schema-qualification:
select * from voucher_receipt ( ...
This only works while your current setting for search_path includes the schema billing.
Better function
You don't need to create a composite type. Unless you need the same type in multiple places just use RETURNS TABLE to define the return type in the function:
CREATE OR REPLACE FUNCTION billing.voucher_receipt (_from timestamptz
, _to timestamptz)
RETURNS TABLE (
ori numeric
, receipt_no numeric
, receipt_date timestamptz
, reg_no varchar
, patient_name varchar
, tot_refund_bill_amount float8
, username varchar) AS
$func$
BEGIN
RETURN QUERY
SELECT b.receipt_no -- AS ori
, cr.RECEIPT_NO
, ??.receipt_date
, cr.reg_no
, ??.patient_name
, ??.tot_refund_bill_amount
, ??.username
FROM billing.tran_counter_receipt cr
JOIN billing.tran_bill b USING (reg_no)
JOIN mas_user u ON u.uid = cr.ent_by
WHERE ??.receipt_date >= _from
AND ??.receipt_date <= _to
AND b.CASH_BOOK = 'GCASH'
AND ??.cash_book = 'REFUND'
END
$func$ LANGUAGE plpgsql;
Notes
Don't call your parameters "date" while they are actually timestamptz.
RETURN QUERY does not require parentheses.
No need for DECLARE out_put voucher%rowtype; at all.
Your format was inconsistent and messy. That ruins readability and that's also where bugs can hide.
This could just as well be a simple SQL function.
Column names in RETURNS TABLE are visible in the function body almost everywhere. table-qualify columns in your query to avoid ambiguities (and errors). Replace all ??. I left in the code, where information was missing.
Output column names are superseded by names in the RETURNS declaration. So AS ori in the SELECT list is just documentation in this case.
Why schema-qualify billing.tran_bill but not mas_user?

Can't create a simple Postgresql function

I'm trying to create an user-defined function in Postgresql:
CREATE FUNCTION get_balance(user_id integer, statuses integer[]) RETURNS INTEGER
AS $$
select SUM(table1.credit)
from table1
inner join table2
on table2.field1 = table1.id
inner join table3
on table3.field1 = table2.id
where table3.status_id in (statuses); $$
LANGUAGE SQL;
The error is:
ERROR: operator does not exist: integer = integer[]
LINE 11: where table3.status_id in (statuses); $$
What am I doing wrong?
This:
table3.status_id in (statuses)
can be simplified for the example into:
regress=> SELECT 1 IN (ARRAY[1,2,3]);
ERROR: operator does not exist: integer = integer[]
LINE 1: SELECT 1 IN (ARRAY[1,2,3]);
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
... but IN expects a literal list, eg:
regress=> SELECT 1 IN (1, 2, 3);
Since you want to pass an array, you'll want to use = ANY(...) which expects an array input:
regress=> SELECT 1 = ANY (ARRAY[1,2,3]);
?column?
----------
t
(1 row)