Is it possible to avoid explicit casts for composite types in plpgsql functions? - postgresql

I am developing a framework that dynamically creates tables for contents storage on PostgreSQL 9.1. One of the API functions allows caller to save a new contents entry by specifying all fields within a given object (say, web form). In order to receive a set of fields framework creates a composite type.
Consider the following code:
CREATE SEQUENCE seq_contents MINVALUE 10000;
CREATE TABLE contents (
content_id int8 not null,
is_edited boolean not null default false,
is_published boolean not null default false,
"Input1" varchar(60),
"CheckBox1" int2,
"TheBox" varchar(60),
"Slider1" varchar(60)
);
CREATE TYPE "contentsType" AS (
"Input1" varchar(60),
"CheckBox1" int2,
"TheBox" varchar(60),
"Slider1" varchar(60)
);
CREATE OR REPLACE FUNCTION push(in_all anyelement) RETURNS int8 AS $push$
DECLARE
_c_id int8;
BEGIN
SELECT nextval('seq_contents') INTO _c_id;
EXECUTE $$INSERT INTO contents
SELECT a.*, b.*
FROM (SELECT $1, true, false) AS a,
(SELECT $2.*) AS b$$ USING _c_id, in_all;
RETURN _c_id;
END;
$push$ LANGUAGE plpgsql;
Now, in order to call this function I have to add explicit cast, like this:
SELECT push(('input1',1,'thebox','slider1')::"contentsType");
Is there a way to avoid explicit cast? As I would like external callers not to deal with casts, i.e. hide the logic behind the PostgreSQL functions. Currently I have such error:
SELECT push(('input1',1,'thebox','slider1'));
ERROR: PL/pgSQL functions cannot accept type record
CONTEXT: compilation of PL/pgSQL function "push" near line 1

Have you considered passing the record variable as its text representation?
In theory, every record variable can be cast to and from text with the normal CAST operator.
Here is the function modified so that in_all has type text and gets casted to "contentsType" in the USING clause:
CREATE OR REPLACE FUNCTION push(in_all text) RETURNS int8 AS $push$
DECLARE
_c_id int8;
BEGIN
SELECT nextval('seq_contents') INTO _c_id;
EXECUTE $$INSERT INTO contents
SELECT a.*, b.*
FROM (SELECT $1, true, false) AS a,
(SELECT $2.*) AS b$$ USING _c_id, in_all::"contentsType";
RETURN _c_id;
END;
$push$ LANGUAGE plpgsql;
Then it can be called like this (no explicit reference to the type)
select push( '(input1,1,thebox,slider1)' );
or like that (explicit record casted to text)
SELECT push(('input1',1,'thebox','slider1')::"contentsType"::text);
That would work not just with "contentsType", but any other record type, assuming the function is able to convert it back to that type.
Also in plpgsql, I assume this should work as well:
ret := push(r::text);
when r is a record variable.

Since you're hard-coding the table name into which you want to insert, and you have a fixed number and type of parameters it needs, I'm not clear on why you need the "contentsType" type at all. Why not eliminate the extra level of parentheses from the function calling, and just pass the four parameters directly? That keeps everything simpler.
CREATE OR REPLACE FUNCTION push(
"Input1" varchar(60),
"CheckBox1" int2,
"TheBox" varchar(60),
"Slider1" varchar(60)
) RETURNS int8 AS $push$
DECLARE
_c_id int8;
BEGIN
SELECT nextval('seq_contents') INTO _c_id;
EXECUTE $$INSERT INTO contents
VALUES ($1, true, false, $2, %3, %4, $5)
$$ USING _c_id, "Input1", "CheckBox1", "TheBox", "Slider1");
RETURN _c_id;
END;
$push$ LANGUAGE plpgsql;
That makes calling the function look like this:
SELECT push('input1',1,'thebox','slider1');
If you're looking to generalized the push() function so that it works for all tables, you'll hit other problems if you get past this one. You won't be able to get past the fact that the function will need to know the table name during execution. If you want to overload the function so that you can have a separate push() for each record type, you need to provide information on the record type somehow. So, if you're looking to do something like this, the short answer to your question is "No."
On the other hand, you may be making this a little harder than it needs to be. I hope you are aware that there is automatically a type created for every table, by the same name as the table. You could probably leverage that to both avoid declaring the type explicitly and to pass a record with the same name as your table -- with dummy entries for the values that the function will fill. I think you could make one totally generic push function, although it might be hard to get past the strong typing issues in plpgsql; writing the function in C might be easier if you're familiar with it.

Related

Error when creating a generated column in Postgresql

CREATE TABLE Person (
id serial primary key,
accNum text UNIQUE GENERATED ALWAYS AS (
concat(right(cast(extract year from current_date) as text), 2), cast(id as text)) STORED
);
Error: generation expression is not immutable
The goal is to populate the accNum field with YYid where YY is the last two letters of the year when the person was added.
I also tried the '||' operator but it was unsuccessful.
As you don't expect the column to be updated, when the row is changed, you can define your own function that generates the number:
create function generate_acc_num(id int)
returns text
as
$$
select to_char(current_date, 'YY')||id::text;
$$
language sql
immutable; --<< this is lying to Postgres!
Note that you should never use this function for any other purpose. Especially not as an index expression.
Then you can use that in a generated column:
CREATE TABLE Person
(
id integer generated always as identity primary key,
acc_num text UNIQUE GENERATED ALWAYS AS (generate_acc_num(id)) STORED
);
As #ScottNeville correctly mentioned:
CURRENT_DATE is not immutable. So it cannot be used int a GENERATED ALWAYS AS expression.
However, you can achieve this using a trigger nevertheless:
demo:db<>fiddle
CREATE FUNCTION accnum_trigger_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
NEW.accNum := right(extract(year from current_date)::text, 2) || NEW.id::text;
RETURN NEW;
END
$$;
CREATE TRIGGER tr_accnum
BEFORE INSERT
ON person
FOR EACH ROW
EXECUTE PROCEDURE accnum_trigger_function();
As #a_horse_with_no_name mentioned correctly in the comments: You can simplify the expression to:
NEW.accNum := to_char(current_date, 'YY') || NEW.id;
I am not exactly sure how to solve this problem (maybe a trigger), but current_date is a stable function not an immutable one. For the generated IDs I believe all function calls must be immutable. You can read more here https://www.postgresql.org/docs/current/xfunc-volatility.html
I dont think any function that gets the date can be immutable as Postgres defines this as "An IMMUTABLE function cannot modify the database and is guaranteed to return the same results given the same arguments forever." This will not be true for anything that returns the current date.
I think your best bet would be to do this with a trigger so on insert it sets the value.

Return bool function with an aliased column name in PostgresQL

I have this pgsql function that checks if a person exists based on given personid. This function returns bool accordingly. From my query, I get the following:
However, I want the resulting column to have an alias so it's more readable among our developers. So instead of getting func_util__check_basicinfo, I want it to be person_exists.
Also the reason why the function is named this way is for our convention purpose.
Here's my query for this function:
CREATE OR REPLACE FUNCTION profile.func_util__check_basicinfo(person_id integer)
RETURNS boolean
LANGUAGE plpgsql
AS $function$
begin
return exists (
select 1
from profile.person_basicinfo pb
where pb.personid = person_id
);
END;
$function$
;
A function's definition doesn't and can't define an alias. You will need to use the alias in the query where you use the function:
select func_util_check_basicinfo(...) as person_exists
from ...
By default a column will be named after the source in the query. So the column name always defaults to the function's name (this is true for all functions in Postgres). If you don't want to specify the column alias manually in every query, your only option is to rename the function to person_exists() (which I would find a much better name than the existing one to begin with)

Access and return result from INSERT INTO in PL/pgSQL function

I am currently learning a lot of PostgreSQL, especially PLPGSQL and am struggling in handling query results in functions.
I want to create a wrapper around a user table and use the result later on and then return it.
In my case the user and account are two different tables and I want to create it in one go.
My first and naïve approach was to build the following:
CREATE OR REPLACE FUNCTION schema.create_user_with_login (IN email varchar, IN password varchar, IN firstname varchar DEFAULT NULL, IN surname varchar DEFAULT NULL)
RETURNS schema.user
LANGUAGE plpgsql
VOLATILE
RETURNS NULL ON NULL INPUT
AS
$$
declare
created_user schema."user";
begin
INSERT INTO schema."user" ("firstname", "surname", "email")
VALUES (firstname, surname, email)
RETURNING * INTO created_user;
// [...] create accounts and other data using e.g. created_user.id
// the query should return the initially created user
RETURN created_user
end;
$$;
This approach does not work, as schema.user has NOT NULL fields (a domain type with that constraint) and will throw an exception for the declared statement:
domain schema."USER_ID" does not allow null values
So maybe it could work, but not with in that constrained environment.
I also tried to use RETURNS SETOF schema.user and directly RETURN QUERY INSERT ...., but this does not return all columns, but instead one column with all the data.
How can I achieve the effect of returning the initial user object as a proper user row while having the data available inside the function?
I am using Postgres 9.6. My version output:
PostgreSQL 9.6.1 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.2 20140120 (Red Hat 4.8.2-16), 64-bit
Issue 1
I also tried to use RETURNS SETOF schema.user and directly RETURN QUERY INSERT ...., but this does not return all columns, but instead
one column with all the data.
Sure it returns all columns. You have to call set-returning functions like this:
SELECT * FROM schema.create_user_with_login;
You have to declare it as RETURNS SETOF foo.users to cooperate with RETURN QUERY.
Issue 2
It's nonsense to declare your function as STRICT (synonym for RETURNS NULL ON NULL INPUT) and then declare NULL parameter default values:
... firstname varchar DEFAULT NULL, IN surname varchar DEFAULT NULL)
You cannot pass NULL values to a function defined STRICT, it would just return NULL and do nothing. While firstname and surname are meant to be optional, do not define the function strict (or pass empty strings instead or something)
More suggestions
Don't call your schema "schema".
Don't use the reserved word user as identifier at all.
Use legal, lower-case, unquoted identifiers everywhere if possible.
Function
All things considered, your function might look like this:
CREATE OR REPLACE FUNCTION foo.create_user_with_login (_email text
, _password text
, _firstname text = NULL
, _surname text = NULL)
RETURNS SETOF foo.users
LANGUAGE plpgsql AS -- do *not* define it STRICT
$func$
BEGIN
RETURN QUERY
WITH u AS (
INSERT INTO foo.users (firstname, surname, email)
VALUES (_firstname, _surname, _email)
RETURNING *
)
, a AS ( -- create account using created_user.id
INSERT INTO accounts (user_id)
SELECT u.user_id FROM u
)
-- more chained CTEs with DML statements?
TABLE u; -- return the initially created user
END
$func$;
Yes, that's a single SQL statement with several data-modifying CTE to do it all. Fastest and cleanest. The function wrapper is optional for convenience. Might as well be LANGUAGE sql. Related:
Insert data in 3 tables at a time using Postgres
I prepended function parameter names with underscore (_email) to rule out naming conventions. This is totally optional, but you have carefully keep track of the scope of conflicting parameters, variables, and column names if you don't.
TABLE u is short for SELECT * FROM u.
Is there a shortcut for SELECT * FROM?
Store results of query in a plpgsql variable?
Three distinct cases:
Single value:
Store query result in a variable using in PL/pgSQL
Single row
Declare row type variable in PL/pgSQL
Set of rows (= table)
There are no "table variables", but several other options:
PostgreSQL table variable
How to use a record type variable in plpgsql?

Getting row values based on array of column names passed to PostgreSQL function

I'm trying to set up full text search in PostgreSQL 9.2. I created a new table to hold the content that I want to search (so that I can search across lots of different types of items), which looks like this:
CREATE TABLE search (
target_id bigint PRIMARY KEY,
target_type text,
fts tsvector
);
CREATE INDEX search_fts ON search USING gin(fts);
Every time a new item gets inserted (or updated) into one of the various tables I want to search across, it should automatically be added to the search table. Assuming that my table looks like the following:
CREATE TABLE item (id bigint PRIMARY KEY, name text NOT NULL, description text);
I created a trigger passing in the column names that I want to be able to search:
CREATE TRIGGER insert_item_search BEFORE INSERT
ON item FOR EACH ROW EXECUTE PROCEDURE
insert_search('{name, description}'::text[]);
Then created a new function insert_search as:
CREATE OR REPLACE FUNCTION insert_search(cols text[]) RETURNS TRIGGER AS $$
BEGIN
INSERT INTO search (target_id, target_type, fts) VALUES (
NEW.id, TG_TABLE_NAME, to_tsvector('english', 'foo')
);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
My question is, how do I pass in the table values based on cols to to_tsvector? Right now, the function is getting called and inserts the id and type correctly, but I don't know the right way to dynamically grab the other values based on the cols argument.
First, to pass arguments, just send them directly:
CREATE TRIGGER insert_item_search BEFORE INSERT
ON item FOR EACH ROW EXECUTE PROCEDURE
insert_search('name', 'description');
And, from PL/pgSQL you will get those arguments as an array, called TG_ARGV. But, the problem is that PL/pgSQL cannot get the values from NEW record based on their names. To do that you can either use a language that lets you do that (like PL/python or PL/perl) or use the hstore extension.
I'd stick with the last one and use hstore (unless you already use one of the other languages to create functions):
CREATE OR REPLACE FUNCTION insert_search() RETURNS TRIGGER AS $$
DECLARE
v_new hstore;
BEGIN
v_new = hstore(NEW); -- convert the record to hstore
FOR i IN 0..(TG_NARGS-1) LOOP
INSERT INTO search (target_id, target_type, fts) VALUES (
NEW.id, TG_TABLE_NAME, to_tsvector('english', v_new -> TG_ARGV[i])
);
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
As you can see above, I used the hstore's operator -> to get the value based on the name (on TG_ARGV[i]).
You can access the parameters specified in the trigger definition with the TG_ARGV variable. You can find documentation on that here. TG_ARGV is an array accessed by a 0 based index. So it would be something like TG_ARGV[0], TG_ARGV[1], and so on.

In postgres (plpgsql), how to make a function that returns select * on a variable table_name?

Basically, at least for proof of concept, I want a function where I can run:
SELECT res('table_name'); and this will give me the results of SELECT * FROM table_name;.
The issue I am having is schema...in the declaration of the function I have:
CREATE OR REPLACE FUNCTION res(table_name TEXT) RETURNS SETOF THISISTHEPROBLEM AS
The problem is that I do not know how to declare my return, as it wants me to specify a table or a schema, and I won't have that until the function is actually run.
Any ideas?
You can do this, but as mentioned before you have to add a column definiton list in the SELECT query.
CREATE OR REPLACE FUNCTION res(table_name TEXT) RETURNS SETOF record AS $$
BEGIN
RETURN QUERY EXECUTE 'SELECT * FROM ' || table_name;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM res('sometable') sometable (col1 INTEGER, col2 INTEGER, col3 SMALLINT, col4 TEXT);
Why for any real practical purpose would you just want to pass in table and select * from it? For fun maybe?
You can't do it without defining some kind of known output like jack and rudi show. Or doing it like depesz does here using output parameters http://www.depesz.com/index.php/2008/05/03/waiting-for-84-return-query-execute-and-cursor_tuple_fraction/.
A few hack around the wall approachs are to issue raise notices in a loop and print out a result set one row at a time. Or you could create a function called get_rows_TABLENAME that has a definition for every table you want to return. Just use code to generate the procedures creations. But again not sure how much value doing a select * from a table, especially with no constraints is other than for fun or making the DBA's blood boil.
Now in SQL Server you can have a stored procedure return a dynamic result set. This is both a blessing and curse as you can't be certain what comes back without looking up the definition. For me I look at PostgreSQL's implementation to be the more sound way to go about it.
Even if you manage to do this (see rudi-moore's answer for a way if you have 8.4 or above), You will have to expand the type explicitly in the select - eg:
SELECT res('table_name') as foo(id int,...)