Can't create a simple Postgresql function - postgresql

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)

Related

Return entire row from table and columns from other tables

I'm using postgresql 14 and trying to return an entire record from a table in addition to columns from different tables. The catch is, that I don't want to write all the titles in the record. I tried working with the guide lines here [1], but I'm getting different errors. Can anyone tell me what I'm doing wrong?
CREATE OR REPLACE FUNCTION public.get_license_by_id(license_id_ integer)
RETURNS TABLE(rec tbl_licenses, template_name character varying, company_name character varying)
LANGUAGE 'plpgsql'
COST 100
VOLATILE SECURITY DEFINER PARALLEL UNSAFE
ROWS 1000
AS $BODY$
DECLARE
BEGIN
CREATE TEMPORARY TABLE tempTable AS (
SELECT
(rec).*, B.company_name, C.template_name
-- Tried using A instead of (rec).* with error: column "a" has pseudo-type record
FROM
(
(SELECT * FROM tbl_licenses) A
LEFT JOIN
(SELECT * FROM tbl_customers) B on A.customer_id = B.customer_id
LEFT JOIN
(SELECT * FROM tbl_templates) C on A.template_id = C.template_id
)
);
UPDATE tempTable
SET license = '1'
WHERE tempTable.license IS NOT NULL;
RETURN QUERY (
SELECT * FROM tempTable
);
DROP TABLE tempTable;
RETURN;
END;
$BODY$;
I'm calling the function like SELECT rec FROM get_license_by_id(1);
but getting:
ERROR: structure of query does not match function result type
DETAIL: Returned type integer does not match expected type tbl_licenses in column 1.
You need to cast the A alias to the correct record type. However the nested derived tables are not necessary for the other tables. If you use a coalesce() for the license column in the SELECT, then you get get rid of the inefficient creation and update of the temp table as well.
CREATE OR REPLACE FUNCTION get_license_by_id(license_id_ integer)
RETURNS TABLE(rec tbl_licenses, template_name character varying, company_name character varying)
LANGUAGE sql
STABLE
AS $BODY$
SELECT a::tbl_licenses, -- this is important
B.company_name, C.template_name
FROM (
select license_id, customer_id, ... other columns ...,
coalesce(license, '1') as license -- makes the temp table unnecessary
from tbl_licenses A
) a
LEFT JOIN tbl_customers B on A.customer_id = B.customer_id
LEFT JOIN tbl_templates C on A.template_id = C.template_id
where a.license_id = license_id_;
$BODY$
;

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$;

How to cast a varchar into an int[] using 'in' operator in PostgreSQL

I am having a hard time about this.
I am trying to cast a varchar containing a list of numbers into an int array, in order to serve an in operator on a where clause.
This is the last version of my code.
create or replace function is_product_in_categories (
_product_id integer,
_cat_list varchar
)
returns boolean
as $$
declare
_n integer;
begin
_n = 0;
select count(*)
into _n
from category_products
where product_id = _product_id and category_id in (_cat_list::int[]);
return _n > 0;
end;
$$ language plpgsql;
select is_product_in_categories(1, '1, 2, 3');
Error is
SQL Error [42883]: ERROR: operator does not exist: integer = integer[]
Hint: No operator matches the given name and argument types. You might need to add explicit type casts.
Where: PL/pgSQL function is_product_in_categories(integer,character varying) line 7 at SQL statement
I have tried several arguments, as '1, 2, 3', '(1, 2, 3)' or '[1, 2, 3]'. Also removing parenthesis near the in operator, etc.
Any idea?
Use string_to_array() to convert a string to a (text) array:
SELECT string_to_array('1, 2, 3', ', ')::int[]; -- use ::int[] to cast to an int array
+---------------+
|string_to_array|
+---------------+
|{1,2,3} |
+---------------+
If you control the string (e.g you are constructing it yourself), you can use either of those two:
SELECT ARRAY[1, 2, 3] -- no need to cast this one
, '{1, 2, 3}'::int[]; -- you have to specify that it's an array, not simply a string value
+-------+-------+
|array |int4 |
+-------+-------+
|{1,2,3}|{1,2,3}|
+-------+-------+
The problem with the in operator is it doesn't admit an array as an argument. Instead it expects a simple list of scalars. See PostgreSQL documentation here https://www.postgresql.org/docs/9.0/functions-comparisons.html#AEN16964
To avoid this limitation the = any combination accepts an array as an argument.
The code ends this way.
create or replace function is_product_in_categories (
_product_id integer,
_cat_list varchar
)
returns boolean
as $$
declare
_n integer;
begin
_n = 0;
select count(*)
into _n
from of_category_products
where product_id = _product_id and category_id = any (_cat_list::int[]);
return _n > 0;
end;
$$ language plpgsql;
select is_product_in_categories(1, '{1, 2, 3}')
Also, the syntax for literal arrays, using {} has been observed, following Bergi comment.
Revise your function declaration and define as variadic integer array:
create or replace function is_product_in_categories (
_product_id integer,
Variadic _cat_list integer[] )
or just as a array of integers:
create or replace function is_product_in_categories (
_product_id integer,
_cat_list integer[] )
Either way you can reduce the function to a single statement.
create or replace function is_product_in_categories3 (
_product_id integer,
_cat_list integer[]
)
returns boolean
language sql
as $$
select
exists (select null
from category_products
where product_id = _product_id and category_id = any(_cat_list)
);
$$;
See here for full example of both.

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

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)

SQL state: 42883 in PostgreSQL 9.3

I have the following table called as test_type which contains two columns namely cola and colb.
Table: test_type
create table test_type
(
cola int,
colb varchar(50)
);
Now I want to create a type with same columns.
Type: type1
create type type1 as
(
cola int,
colb varchar(50)
);
Here I have created function in which I am passing type name type1 to insert the data to the
table test_type.
--Creating Function
create or replace function fun_test ( p_Type type1 )
returns void
as
$$
begin
insert into test_type(cola,colb)
select cola,colb from p_type
EXCEPT
select cola,colb from test_type;
end
$$
language plpgsql;
---Calling Function
SELECT fun_test(1,'Xyz');
Error Details:
ERROR: function fun_test(integer, unknown) does not exist
SQL state: 42883
Hint: No function matches the given name and argument types. You might need to add explicit type casts.
Character: 8
You need to "pack" the arguments together: (1,'xs'), so that postgres recognise them as single argument of type type1:
SELECT fun_test((1,'xs'));
For a better readability you can cast the argument to type1 (not really necessary):
SELECT fun_test((1,'xs')::type1);
If the purpose of the function is to to insert the values only if they are not already contained in the table, you could change your code so:
create or replace function fun_test ( p_Type type1 )
returns void AS $$
BEGIN
INSERT INTO test_type(cola,colb)
SELECT p_Type.cola,p_Type.colb
EXCEPT
SELECT cola,colb FROM test_type;
END;
$$ language plpgsql;
But this syntax is my opinion not good readable. This statement looks better:
...
BEGIN
PERFORM 0 FROM test_type WHERE (cola, colb) = p_Type;
IF NOT FOUND THEN
INSERT INTO test_type(cola,colb) VALUES (p_Type.cola,p_Type.colb);
END IF;
END;
...