In postgresql documentation, there is the entry regarding the "CREATE OPERATOR". I'm literally desperate for a compound assignment operator, C or C# like style, in my PL/PGSQL code. Ideally it would work like this
v_my_text += 'some more text to be added to existing value of v_my_text';
This would of course be the equivalent of
v_my_text := v_my_text || 'some more text...';
I was not able to find any example around the assignment operators. Is something like this possible?
According to feedbacks I was getting, creating += operator is currently not possible. Yet, #Bergi gave me an idea with inout parameters which can simplify my code.
create or replace procedure add_line
(
p_text_body inout text,
p_new_text in text
)
language plpgsql
as $$
begin
p_text_body := p_text_body || p_next_text || chr(10) || chr(13);
end; $$
It would be used like call add_line(v_output_report, 'New text line here...');
Any better ideas are welcome. See comments on question for more context. Regards.
There is no need for procedure with INOUT parameter (or starting with v14 an OUT parameter). Instead just build an SQL function. (see demo)
create or replace function add_line
( p_text_body in text
, p_new_text in text
)
returns text
language sql
as $$
select concat(p_text_body, E'\n', p_new_text);
$$;
Notes:
Use E'\n' (4.1. Lexical Structure: 4.1.2.2. String Constants with C-Style Escapes) rather than chr(10) || chr(13). It adjusts to the proper code for the operating system.
The normal assignment operator in Postgres is just =. The += is strictly to maintain compatibility with Oracle plsql.
The concat function is perhaps better than the concatenation operator (||) here as it handles NULLs. concat('A', null) or concat(null, 'A' ) both return 'A' where as 'A' || null or null || 'A' both return null.
Related
May I know on how to call an array in stored procedure? I tried to enclosed it with a bracket to put the column_name that need to be insert in the new table.
CREATE OR REPLACE PROCEDURE data_versioning_nonull(new_table_name VARCHAR(100),column_name VARCHAR(100)[], current_table_name VARCHAR(100))
language plpgsql
as $$
BEGIN
EXECUTE ('CREATE TABLE ' || quote_ident(new_table_name) || ' AS SELECT ' || quote_ident(column_name) || ' FROM ' || quote_ident(current_table_name));
END $$;
CALL data_versioning_nonull('sales_2019_sample', ['orderid', 'product', 'address'], 'sales_2019');
Using execute format() lets you replace all the quote_ident() with %I placeholders in a single text instead of a series of concatenated snippets. %1$I lets you re-use the first argument.
It's best if you use ARRAY['a','b','c']::VARCHAR(100)[] to explicitly make it an array of your desired type. '{"a","b","c"}'::VARCHAR(100)[] works too.
You'll need to convert the array into a list of columns some other way, because when cast to text, it'll get curly braces which are not allowed in the column list syntax. Demo
It's not a good practice to introduce random limitations - PostgreSQL doesn't limit identifier lengths to 100 characters, so you don't have to either. The default limit is 63 bytes, so you can go way, way longer than 100 characters (demo). You can switch that data type to a regular text. Interestingly, exceeding specified varchar length would just convert it to unlimited varchar, making it just syntax noise.
DBFiddle online demo
CREATE TABLE sales_2019(orderid INT,product INT,address INT);
CREATE OR REPLACE PROCEDURE data_versioning_nonull(
new_table_name TEXT,
column_names TEXT[],
current_table_name TEXT)
LANGUAGE plpgsql AS $$
DECLARE
list_of_columns_as_quoted_identifiers TEXT;
BEGIN
SELECT string_agg(quote_ident(name),',')
INTO list_of_columns_as_quoted_identifiers
FROM unnest(column_names) name;
EXECUTE format('CREATE TABLE %1$I.%2$I AS SELECT %3$s FROM %1$I.%4$I',
current_schema(),
new_table_name,
list_of_columns_as_quoted_identifiers,
current_table_name);
END $$;
CALL data_versioning_nonull(
'sales_2019_sample',
ARRAY['orderid', 'product', 'address']::text[],
'sales_2019');
Schema awareness: currently the procedure creates the new table in the default schema, based on a table in that same default schema - above I made it explicit, but that's what it would do without the current_schema() calls anyway. You could add new_table_schema and current_table_schema parameters and if most of the time you don't expect them to be used, you can hide them behind procedure overloads for convenience, using current_schema() to keep the implicit behaviour. Demo
First, change your stored procedure to convert selected columns from array to csv like this.
CREATE OR REPLACE PROCEDURE data_versioning_nonull(new_table_name VARCHAR(100),column_name VARCHAR(100)[], current_table_name VARCHAR(100))
language plpgsql
as $$
BEGIN
EXECUTE ('CREATE TABLE ' || quote_ident(new_table_name) || ' AS SELECT ' || array_to_string(column_name, ',') || ' FROM ' || quote_ident(current_table_name));
END $$;
Then call it as:
CALL data_versioning_nonull('sales_2019_sample', '{"orderid", "product", "address"}', 'sales_2019');
I am writing 1 PostgreSQL function for some operation.
Writing SQL migration for that function but facing formatting error as liquibase is not able to recognize some portion.
Function Liquibase Migration:
CREATE OR REPLACE FUNCTION schema.fncn(trId integer, sts integer, stIds character varying)
RETURNS double precision
LANGUAGE plpgsql
AS '
DECLARE
abc integer;
query CHAR(1500);
xyz integer;
BEGIN
query := ''select sum(t.a)
FROM schema.tbl t
where t.id in(1,2)
and t.status ='' || sts ||
'' and t.status <> 2
and t.tr_id ='' || trId ||
'' and t.sw in('''', ''N'')'';
IF stIds is not null then
query := query || '' AND t.st_id IN ('' || stIds || '')'';
ELSE
END IF;
EXECUTE query INTO abc;
SELECT abc INTO xyz;
RETURN xyz;
END;
'
;
Following error it throwing:
Caused by: org.postgresql.util.PSQLException: ERROR: syntax error at or near "N"
Reason: liquibase.exception.DatabaseException: ERROR: syntax error at or near "N"
Any suggestion what I am missing?
The immediate problem is the nesting of ' of single quotes. To make that easier, use dollar quoting for the function body. You can nest dollar quoted string by choosing different delimiters.
To avoid any problems with concatenation of parameters, use parameter place holders in the query and pass the values with the USING clause. That will however require two different execute calls.
I assume stIds is a comma separated string of values. To use that as a (single) placeholder, convert it to an array using string_to_array() - or even better: change the type of the input parameter to text[] and pass an array directly.
The query variable is better defined as text, don't use char. There is also no need to copy the result of the query into a different variable (which by the way would be more efficient using xyz := abc; rather than a select into)
CREATE OR REPLACE FUNCTION schema.fncn(trId integer, sts integer, stIds character varying)
RETURNS double precision
LANGUAGE plpgsql
AS
$body$
DECLARE
abc integer;
query text;
BEGIN
query := $q$ select sum(t.a)
FROM schema.tbl t
where t.id in (1,2)
and t.status = $1
and t.status <> 2
and t.tr_id = $2
and t.sw in ('''', 'N') $q$;
IF stIds is not null then
query := query || $sql$ AND t.st_id = ANY (string_to_array($4, ',') $sql$;
EXECUTE query INTO abc
using trid, sts, stids;
ELSE
EXECUTE query INTO abc
using trid, sts;
END IF;
RETURN abc;
END;
$body$
;
Note that in the Liquibase change, you must use splitStatements=false in order to run this without errors.
I am trying to do a case-insensitive partial search (contains string) on a property value - stored in a jsonb field in postgres.
The search is looking for a value within the title column of table destination which has an array of elements as follows:
[{"key": "EN", "text":"london and milk"},{"key": "FR", "text":"Edinburgh with milk and honey"}]
I have created a GIN index on the title field and a function to deal with the search.
CREATE OR REPLACE FUNCTION search(query_string character varying)
RETURNS SETOF destination
LANGUAGE 'plpgsql'
AS $BODY$
begin
return query select *
from destination
--where title #? '$.* ? (# like_regex ' || query_string || ' flag "i")';
where title #? '$.* ? (# like_regex ".*milk.*" flag "i")';
end;
$BODY$;
So the function works nicely if the regexp string is hardcoded (as shown above), but the search should be based on the incoming query_string. The commented line in the function shows an attempt to try to include the parameter in the query. (this will result in unterminated string constant error)
How can I exchange the hard-coded milk to parameter search_query?
Are there other (simpler) ways that would yield the same end result?
Your problem is one of precedence. #? and '||' are tied and are processed left to right, so you are applying #? to only a fragment of the string not the completely built string. Then you are trying to concat things to the Boolean result of #?. You can fix this by constructing the string inside parentheses. A side affect of this is that you then have to cast it to jsonpath explicitly.
where title #? ( '$.* ? (# like_regex "' || query_string || '" flag "i")' )::jsonpath;
But I think it would be cleaner to construct the jsonpath in a variable, rather than on the fly in the query itself. Could someone inject something into the jsonpath string that could do something nasty? I don't know enough about jsonpath to rule that out.
(code part of the suggested solution edited by question author to include the double quotes missing - see comment)
I am trying to return the movie name and the number of cast and crew when given a text. When I input the string and am using ilike, my query returns no matching titles. I created a view previously that has the movie titles and the number of crew to be input in the function.
My code is:
create or replace view movies_crew as
select movies.id, movies.title, principals.role
from movies
join principals on principals.movie_id=movies.id
where principals.role <> 'producer'
;
create or replace view movie_makers as
select movies_crew.title, count(movies_crew.title) as ncrew
from movies_crew
where movies_crew.title = 'Fight Club'
group by movies_crew.title;
CREATE or REPLACE function Q11(partial_title text)
RETURNS SETOF text
AS $$
DECLARE
title text;
BEGIN
for title in
select movie_makers.title, movie_makers.ncrew
from movie_makers
where movie_makers.title ilike '%$1%'
loop
return next movie_makers.title||'has'||movie_makers.ncrew||'cast and crew';
end loop;
if(not found) then
return next 'No matching titles';
end if;
END;
$$ LANGUAGE plpgsql;
select * from q11('Fight Club')
My database is: https://drive.google.com/file/d/1NVRLiYBVbKuiazynx9Egav7c4_VHFEzP/view?usp=sharing
Your immediate quoting issue aside (has been addressed properly by Jeff), the function can be much simpler and faster like this:
CREATE or REPLACE FUNCTION q11(partial_title text)
RETURNS SETOF text
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY
SELECT m.title || ' has ' || m.ncrew || ' cast and crew'
FROM movie_makers m
WHERE m.title ~* $1;
IF NOT FOUND THEN
RETURN NEXT 'No matching titles';
END IF;
END
$func$;
Major points:
Your function was still broken. References to movie_makers.title and movie_makers.ncrew wouldn't work that way. I fixed it.
Use RETURN QUERY instead of the loop. This way we also do not need to use or even declare any variables at all. See:
How to return result of a SELECT inside a function in PostgreSQL?
Optionally use the case insensitive regular expression match operator ~*. (Simpler, not faster.)
Difference between LIKE and ~ in Postgres
Either way, you may want to escape special characters. See:
Escape function for regular expression or LIKE patterns
Aside: hardly makes sense to filter on a view that already selects 'Fight Club' as its only row. For a meaningful search, you wouldn't use these views ...
ilike '%$1%'
$1 is not interpolated when inside single quotes, so you are searching for the literal characters $ and 1.
You could instead do:
ilike '%'||$1||'%'
I am trying to execute the following dynamic sql, but I could not figure out how to do it:
DROP FUNCTION f_mycross(text, text);
EXECUTE ('CREATE OR REPLACE FUNCTION f_mycross(text, text)
RETURNS TABLE ("registration_id" integer, '
|| (SELECT string_agg(DISTINCT pivot_headers, ',' order by pivot_headers)
FROM (SELECT DISTINCT '"' || qid::text || '" text' AS pivot_headers
FROM answers) x)
|| ') AS ''$libdir/tablefunc'',''crosstab_hash'' LANGUAGE C STABLE STRICT;')
I am relatively new to PostgreSQL.
Like a_horse commented, EXECUTE is not an SQL command. It's a PL/pgSQL command and can only be used in a function body or DO statement using this procedural language. Like:
DROP FUNCTION IF EXISTS f_mycross(text, text);
DO
$do$
BEGIN
EXECUTE (
SELECT 'CREATE OR REPLACE FUNCTION f_mycross(text, text)
RETURNS TABLE (registration_id integer, '
|| string_agg(pivot_header || ' text', ', ')
|| $$) AS '$libdir/tablefunc', 'crosstab_hash' LANGUAGE C STABLE STRICT$$
FROM (SELECT DISTINCT quote_ident(qid::text) AS pivot_header FROM answers ORDER BY 1) x
);
END
$do$; -- LANGUAGE plpgsql is the default
I added some improvements and simplified the nested SELECT query.
Major points
Add IF EXISTS to DROP FUNCTION unless you are certain the function exists or you want to raise an exception if it does not.
DISTINCT in the subquery is enough, no need for another DISTINCT in the outer SELECT.
Use quote_ident() to automatically double-quote identifiers where necessary.
No parentheses required around the string we feed to EXECUTE.
Simpler nested quoting with $-quotes.
Insert text with single quotes in PostgreSQL
We can apply ORDER BY in the subquery, which is typically much faster than adding ORDER BY in the outer aggregate function.