Upon insert, how can I programmatically convert a null value to the column's default value? - postgresql

I have a table in for which I have provided default values for some of its columns. I want to create a function with arguments corresponding to the columns that will insert a record in the table after modifying any null value to the column's default value.I dont want to programmatically construct the query based on which arguments are null. Essentially I would like something like
INSERT into Table (c1, c2, c3, c4)
Values (coalesce(somevar, DEFAULT(c1)), ...)
Is this possible? I ve seen that mysql can do this. Does postgres offer anything similar?
I am using version 9.1
UPDATE: This question provides some interesting solution but unfortunately the results are always text. I would like to get the default value as its true datatype so that I can use it for inserting it. I have tried to find a solution that will cast the default value from text to its datatype (which is provided as text) but I can't find a way:
SELECT column_name, column_default, data_type
FROM information_schema.columns
WHERE (table_schema, table_name) = ('public', 'mytable')
AND column_name = 'mycolumn'
ORDER BY ordinal_position;
The above returns the column_default and data_type as text so how can I cast the column_default to the value of data_type? If I could do this, then my problem would be solved.

If the table definition accepts an INSERT with the default values for all columns, the two-steps method below may work:
CREATE FUNCTION insert_func(c1 typename, c2 typename, ...)
RETURNS VOID AS $$
DECLARE
r record;
BEGIN
INSERT into the_table default values returning *,ctid INTO r;
UPDATE the_table set
col1=coalesce(c1,r.col1),
col2=coalesce(c2,r.col2),
...
WHERE the_table.ctid=r.ctid;
END;
$$ language plpgsql;
The trick is to get all the default values into the r record variable while inserting and use them subsequently in an UPDATE to replace any non-default value. ctid is a pseudo-column that designates the internal unique ID of the row that has just been inserted.
Caveat: this method won't work if some columns have a default null value AND a non-null check (or any check that implies that the default value is not accepted), since the first INSERT would fail.

I ve worked around my problem with a solution similar to Daniel's by creating a temp table with LIKE and INCLUDING DEFAULTS clauses in order to match my rowtype, then i use
INSERT INTO temp_table (c1, c2, ...) VALUES(x1, DEFAULT, ..)
using the default keyword for whatever column i am interested in. Then I insert to the real table by selecting from the temporary and using
VALUES( x1, coalesce(x2, temp_table.c2), ...).
I dont like it, but it works ok: I can select which on which columns I would like to do this "null-replace-with-default" check and it could work for many rows with one pass if I overload my function to accept a record array.

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.

How can I set a default for column B to be the value in column A in PostgreSQL?

How can I set a default for column B to be the value in column A?
I know, it is possible in Microsoft SQL Server:
http://www.ideaexcursion.com/2010/04/19/default-column-value-to-identity-of-different-column/
Is it possible in PostgreSQL?
The linked example shows how to intialize one column with the value of the identity column of the same table.
That is possible in Postgres
create table identdefault
(
a serial not null,
b int not null default currval('identdefault_a_seq')
);
serial will create a sequence in the background that is named tablename_column_seq thus we know that the sequence for identdefault.a will be named identdefault_a_seq and we can access the last value through the currval function.
Running:
insert into identdefault default values;
insert into identdefault default values;
insert into identdefault default values;
select *
from identdefault
will output:
a | b
--+--
1 | 1
2 | 2
3 | 3
This seems to only work with Postgres 9.4, when I tried that with 9.3 (on SQLFiddle) I got an error. But in that case it is possible as well - you just can't use the "shortcut" serial but need to create the sequence explicitly:
create sequence identdefault_a_seq;
create table identdefault
(
a int not null default nextval('identdefault_a_seq'),
b int not null default currval('identdefault_a_seq')
);
insert into identdefault default values;
insert into identdefault default values;
insert into identdefault default values;
If you want to have an identical definition as with the serial column, you just need to make the sequence belong to the column:
alter sequence identdefault_a_seq owned by identdefault.a;
SQLFiddle: http://sqlfiddle.com/#!15/0aa34/1
The answer to the much broader question "How can I set a default for column B to be the value in column A?" is unfortunately: no, you can't (see klin's comment)
You can't do it with an actual DEFAULT, but it's trivial with a BEFORE trigger.
CREATE OR REPLACE FUNCTION whatever() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.onecol := NEW.othercol;
RETURN NEW;
END;
$$;
CREATE TRIGGER whatever_tg
BEFORE INSERT ON mytable
FOR EACH ROW EXECUTE PROCEDURE whatever();
"You cannot do that" and postgres don't go well together. There's almost always a way you can do that (whatever "that" turns out to be).
The question is more like: How do you want to do it?
One way, that is nice to DB-Admins would be: Create a before-Trigger, manipulate the new row before it is written.
If your rules to create that new column are very fancy: Turn to one of the embedded languages (like perl).
So: Is it possible? Of course it is.

Default values in Postgres view with triggered instead-of update

I was planning on having a view with instead of insert trigger. There seems to be a problem with inserting a default value though.
Having the trigger set up as below, the following query fails
INSERT INTO v1.clients (foo) VALUES ('bar')
this returns a
null value in column "is_admin" violates not-null constraint
even though the underlying data.users (not null) table has a default value set.
What I'd say happens is the view translates all missing values to null and the instead of is applied, trying to insert null to a not null default false column.
Can I somehow set up the trigger to instead of trying to insert null to insert the default value? Having coalesce(NEW.is_admin, default) in the relevant insert in the trigger is a syntax error. I would rather not duplicate the default value manually in trigger.
Is this supported in postgres? What would be the best approach to split a view of two tables to those tables, while allowing default values?
definitions:
CREATE OR REPLACE VIEW v1.clients AS
SELECT
c.id, c.foo,
u.id user_id, u.is_admin
FROM data.clients c
INNER JOIN data.users u ON u.client_id = c.id;
CREATE FUNCTION data.separate_client_user_data()
RETURNS TRIGGER AS $$
DECLARE
client_id clients.id%TYPE;
BEGIN
INSERT INTO data.clients (foo) VALUES (NEW.foo) RETURNING id INTO client_id;
INSERT INTO data.users (client_id, is_admin)
VALUES (client_id, NEW.is_admin);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_data_trigger
INSTEAD OF INSERT ON v1.clients
FOR EACH ROW EXECUTE PROCEDURE data.separate_client_user_data();
Late to the game, I know, but I have extra insight I want to share.
Note that, as Patrick said, the NEW variable that the TRIGGER PROCEDURE uses contains all the fields - with NULLs added for any fields that were not originally part of your INSERT statement.
As you pointed out, the underlying table has a default value for the is_admin field, and unfortunately that default value doesn't get a chance to be seen, because the NEW variable already has NEW.is_admin = NULL.
However, I want to point out a different solution from Patrick's. It's true that you can hand-code the IF checks (essentially re-implementing the default value logic!) in your plpgsql function. But this may feel non-optimal from a DRY perspective. Here was the key for me when I had your problem:
TL/DR
The default values of the view itself are honored when building the NEW variable. Therefore if you do ALTER TABLE <view_name> ALTER is_admin SET DEFAULT false it should cause false to be passed in to NEW.is_admin, instead of NULL.
Hope that helps!
In a trigger function, the NEW and OLD implicit parameters always contain all the fields of the underlying table or view, with NULL assigned to fields for which no data is available. This is the correct behaviour, otherwise you could never assign a value to a field with no data.
In case you have a field with a NULL value and you want to get the DEFAULT value upon INSERT instead, you should test for it prior to doing the INSERT:
CREATE FUNCTION data.separate_client_user_data() RETURNS trigger AS $$
DECLARE
cid clients.id%TYPE; -- Don't use variable with same name as a column
BEGIN
INSERT INTO data.clients (foo) VALUES (NEW.foo) RETURNING id INTO cid;
IF NEW.is_admin IS NULL THEN
INSERT INTO data.users (client_id)
VALUES (cid); -- Use default value for is_admin
ELSE
INSERT INTO data.users (client_id, is_admin)
VALUES (cid, NEW.is_admin);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Postgres Insert Into View Rule with Returning Clause

I am attempting to allow insert statements with a returning clause into a view in Postgres v9.4, but am struggling with the syntax. This is how I want to call the insert statement:
CREATE VIEW MyView AS SELECT a.*, b.someCol1
FROM tableA a JOIN tableB b USING(aPrimaryKey);
INSERT INTO MyView (time, someCol) VALUES (someTime, someValue) RETURNING *;
INSERT INTO MyView (someCol) VALUES (someValue) RETURNING *;
Note that the default for time is NOW(). This is what I have so far:
CREATE RULE MyRuleName AS ON INSERT TO MyView DO INSTEAD (
INSERT INTO tableA (time) VALUES COALESCE(NEW.time, NOW());
INSERT INTO tableB (aPrimaryKey, someCol)
VALUES (CURRVAL('tableA_aPrimaryKey_seq'), NEW.someValue);
);
The above works to insert the value, but I am struggling to try and figure out how to add the returning statement. I have tried the following without success:
CREATE RULE MyRuleName AS ON INSERT TO MyView DO INSTEAD (
INSERT INTO tableA (time) VALUES COALESCE(NEW.time, NOW())
RETURNING *, NEW.someValue;
INSERT INTO tableB (aPrimaryKey, someCol)
VALUES (CURRVAL('tableA_aPrimaryKey_seq'), NEW.someValue);
);
-- ERROR: invalid reference to FROM-clause entry for table "new"
CREATE RULE MyRuleName AS ON INSERT TO MyView DO INSTEAD (
WITH a AS (INSERT INTO tableA (time)
VALUES COALESCE(NEW.time, NOW()) RETURNING *)
INSERT INTO tableB (aPrimaryKey, someCol)
SELECT aPrimaryKey, NEW.someValue FROM a RETURNING *;
);
-- ERROR: cannot refer to NEW within WITH query
Argh! Does anyone know of a way to add a returning statement that gets the primary key (SERIAL) and time (TIMESTAMP WITH TIME ZONE) added to the database in the first insert, along with the value of someCol in the second insert? Thanks!
You are much better off using an INSTEAD OF INSERT trigger here:
CREATE FUNCTION MyFuncName() RETURNS trigger AS $$
DECLARE
id integer;
BEGIN
INSERT INTO tableA (time) VALUES COALESCE(NEW.time, NOW()) RETURNING aPrimaryKey INTO id;
INSERT INTO tableB (aPrimaryKey, someCol1) VALUES (id, NEW.someValue);
RETURN NEW;
END; $$ LANGUAGE PLPGSQL;
CREATE TRIGGER MyView_on_insert INSTEAD OF INSERT ON MyView
FOR EACH ROW EXECUTE PROCEDURE MyFuncName();
Checking the current value of a sequence to see what was inserted in another table is bad bad bad practice. Even while you are here in a single transaction, don't do it.
You are confused about the issue of RETURNING information, because I am confused too when I read your question. Inside of a function use the INTO clause to populate locally declared variables to hold record values which you can then use in subsequent statements. Outside of a function, use the RETURNING clause as you do in your top-most code snippet.
I don't agree with the the hint ("use triggers instead of rules"), because triggers don't allow RETURNING. As written in the Postgresql docu it is a little bit tedious to write the right return list. If you keep the following in mind, it works:
You can only use columns from the original table to form a list which returns columns for the view (!). This means that you have to repeat the view expressions including all subqueries. (using WHERE instead of JOIN ... ON). Additionally you have to replace the NEW table by the original table name.

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

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.