Restrict insert into column with default value - sql

Taking the following schema as an example:
CREATE TABLE public."RiskHeaders"
(
"UID" uuid NOT NULL DEFAULT uuid_generate_v4(),
"Name" character varying(35) NOT NULL
)
Is it possible to prevent INSERT statements including values for the UID column?
For avoidance of doubt, I wish for all values within the UID column to be randomly generated via the default value function.
I had hoped that I could use an INSERT trigger to see whether UID had been populated but that doesn't appear to be possible:
BEGIN
IF TG_OP = 'INSERT' THEN
IF TG_WHEN = 'BEFORE' THEN
RAISE NOTICE 'Before: Old = % New = %', OLD."UID", NEW."UID";
ELSIF TG_WHEN = 'AFTER' THEN
RAISE NOTICE 'After: Old = % New = %', OLD."UID", NEW."UID";
END IF;
END IF;
RETURN NEW;
END;
Running INSERT INTO "RiskHeaders"("Name") VALUES ('Test'); gives the following console output:
NOTICE: Before: Old = <NULL> New = ccc6757f-d1a2-4849-828d-017d2b738c9d
NOTICE: After: Old = <NULL> New = ccc6757f-d1a2-4849-828d-017d2b738c9d
Running INSERT INTO "RiskHeaders"("UID", "Name") VALUES (uuid_generate_v4(), 'Test1'); gives the same output (albeit with a different UUID).
Thank you in advance for any suggestions and hopefully this isn't too daft a question!
Many thanks for all the suggestions!
Given my (reckless?) determination to keep the UID column as a uuid type and prevent the user from specifying any values on INSERT, I have come up with the following work-around(/bodge):
I am now using the following schema:
CREATE TABLE public."RiskHeaders"
(
"UID" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'::uuid,
"Name" character varying(35) NOT NULL
}
Taking #stickybit's suggestion to use a trigger... I have come up with a trigger to the effect of:
IF TG_OP = 'INSERT' THEN
IF TG_WHEN = 'BEFORE' THEN
IF NEW."UID" <> '00000000-0000-0000-0000-000000000000'::uuid THEN
RAISE EXCEPTION 'A UID cannot be specified when creating a risk.';
ELSE
NEW."UID" := uuid_generate_v4();
RETURN NEW;
END IF;
...
END IF;
END IF;
...
The drawbacks from this are:
The schema doesn't make it clear that UIDs are actually being randomly assigned.
The user could specify a zero UID which would then get silently replaced.

Related

How to write a postgresql TRIGGER that raises Warning message after INSERTING a certain incorrect numeric value into the EXISTING sql table column?

I need to add a trigger that raises a warning message when a certain out of bounds (not 2, 3, 5-7) numeric value is inserted into or altered in an EXISTING row in a "grade" column in the sql table. This code example raises such a message ONLY when a NEW row is created.
How to raise the message when a value in the EXISTING row is altered?
Values in the "grade" column are tied via key to a column in another table "grade_salary" where they are stored. How to write the insertion/alteration check in such a way that raises the message without specifying the concrete correct values (2, 3, 5-7), but stating only that "IF changed value lies outside of the values specified in column "grade" of the table "grade_salary" THEN raise the error message" (and not let the value be modified)?
CREATE TRIGGER person
BEFORE INSERT ON hr."position"
FOR EACH ROW EXECUTE PROCEDURE person();
CREATE OR REPLACE FUNCTION person()
RETURNS TRIGGER
SET SCHEMA 'hr'
LANGUAGE plpgsql
AS $$
BEGIN
IF ((NEW.grade < 2) or (NEW.grade > 3 and NEW.grade < 5)
or (NEW.grade > 7)) THEΝ
RAISE EXCEPTION 'Incorrect value';
END IF;
RETURN NEW;
END;
$$;
One checks whether the new value corresponds to an existing one in a column with:
IF new_value not in (SELECT DISTINCT grade FROM grade_salary)
THEN RAISE EXCEPTION 'Inadmissible value.'
In full:
CREATE OR REPLACE FUNCTION person()
RETURNS TRIGGER
SET SCHEMA 'hr'
LANGUAGE plpgsql
AS $$
declare
new_value numeric;
begin
select new.grade from "position" into new_value;
IF new_value not in (SELECT DISTINCT grade FROM grade_salary)
THEN RAISE EXCEPTION 'Inadmissible value.';
END IF;
RETURN NEW;
END;
$$;

How to resolve duplicate key error without conflict with postgresql 11.2

insertion code
INSERT INTO employee (pid, pname, desig, dept, lts_i, lts_O, p_status) VALUES %s \
ON CONFLICT (pid) DO UPDATE SET \
(pname, desig, dept, lts_i, lts_O, p_status) = \
(EXCLUDED.pname, EXCLUDED.desig, EXCLUDED.dept, EXCLUDED.lts_i, EXCLUDED.lts_O, EXCLUDED.p_status) \
RETURNING *
If i insert such like above then it's working good. Instead of CONFLICT i have used a function the following
CREATE FUNCTION employee_db(
pid1 integer,
pname1 text,
desig1 text,
dept1 text,
lts_i1 time,
lts_o1 time,
p_status1 text
) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
-- note that "a" must be unique
UPDATE employee SET (lts_i, lts_o, p_status) = (lts_i1, lts_o1, p_status1) WHERE pid = pid1;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO employee(pid, pname, desig, dept, lts_i, lts_o, p_status) VALUES (pid1, pname1, desig1, dept1, lts_i1, lts_o1, p_status1);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing, and loop to try the UPDATE again
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
that takes some argument
SELECT merge_db(12, 'Newton', 'director', 'd1', '10:00:26', '00:00:00', 'P-Status')"
but when i update lts_i, lts_O and p_status within same id(12)
SELECT merge_db(12, 'Newton', 'director', 'd1', '12:10:22', '02:30:02', 'active')"
then it also showing duplicate key error.
I don't want to use here CONFLICT, because of i have a UPDATE RULE on the same Table and already postgresql says that "The event is one of SELECT, INSERT, UPDATE, or DELETE. Note that an INSERT containing an ON CONFLICT clause cannot be used on tables that have either INSERT or UPDATE rules. Consider using an updatable view instead."
Update Rule
CREATE RULE log_employee AS ON UPDATE TO employee
WHERE NEW.lts_i <> OLD.lts_i or NEW.lts_O <> OLD.lts_O
DO UPDATE employee set today = current_date where id = new.id;
if lts_i, lts_o or p_status is update then will be insert current_date into "today" field in the same employee table.
But definitely i need RULE, In this situation what should i do?
Any help would be appreciated.
Thanks.
You should use a trigger for that.
The trigger function:
create function emp_trigger_func()
returns trigger
as
$$
begin
new.today := current_date;
return new;
end;
$$
language plpgsql;
The condition on when that columns should be update is better done in the trigger definition to avoid unnecessary firing of the trigger
create trigger update_today
before update on employee
for each row
when (NEW.lts_i <> OLD.lts_i or NEW.lts_O <> OLD.lts_O)
execute procedure emp_trigger_func();
Note that <> doesn't properly deal with NULL values. If lts_i or lts_o can contain null values, then firing condition is better written as:
when ( NEW.lts_i is distinct from OLD.lts_i
or NEW.lts_O is distinct from OLD.lts_O)
This will also catch a change from or to a null value.

Trigger function to delete certain rows from the same table

I'm trying to create a Trigger/Function in Postgres that will check, upon an insert to a table, whether or not there is already another post by a different member with the same content. If there is a post, this function will not insert the new one and leave the table unchanged. Otherwise, it will be added.
So far, the trigger and function look like:
Trigger:
CREATE TRIGGER isPostUnique
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE PROCEDURE deletePost();
Function:
CREATE FUNCTION deletePost() RETURNS TRIGGER AS $isPostUnique$
BEGIN
IF (EXISTS (SELECT * FROM posts p1, posts p2
WHERE (p1.userID <> p2.userID)
AND (p1.content LIKE p2.content)))
THEN
DELETE FROM NEW WHERE (posts.postID = NEW.postID);
RETURN NEW;
END IF;
END;
$isPostUnique$ LANGUAGE plpgsql;
Adding the function and trigger works without any errors, but when I try to run the following query to test it: INSERT INTO posts VALUES (7, 3, 'test redundant post', 10, 1); I get this error
ERROR: relation "new" does not exist
LINE 1: DELETE FROM NEW WHERE (posts.postID = NEW.postID)
^
QUERY: DELETE FROM NEW WHERE (posts.postID = NEW.postID)
CONTEXT: PL/pgSQL function dp() line 7 at SQL statement
I am aware that you can't use 'NEW' in FOR EACH ROW inserts, but I have no other idea of how to accomplish this.
Updated answer for updated question
Of course you can use NEW in FOR EACH ROW trigger function. You just can't direct a DELETE statement at it. It's a row type (data type HeapTuple to be precise), not a table.
To abort the INSERT silently (no exception raised) if the same content is already there ...
CREATE FUNCTION deletePost()
RETURNS TRIGGER AS
$func$
BEGIN
IF EXISTS (
SELECT 1
FROM posts p
WHERE p.content = NEW.content
-- AND p.userID <> NEW.userID -- I doubt you need this, too?
) THEN
RETURN NULL; -- cancel INSERT
ELSE
RETURN NEW; -- go ahead
END IF;
END
$func$ LANGUAGE plpgsql;
Of course this only works for a trigger ...
...
BEFORE INSERT ON posts
...
Unique index
A UNIQUE constraint or a unique index (almost the same effect) might be a superior solution:
CREATE UNIQUE INDEX posts_content_uni_idx (content);
Would raise an exception at the attempt to insert a duplicate value. No trigger necessary.
It also provides the very well needed index to speed up things.

Use DEFAULT value if empty string

How can I tell Postgres that a column with an empty string should use the DEFAULT value?
Using CHECK doesn't seem to work here.
A check constraint will only prevent putting such a value into the column. It will not magically replace the supplied value with the default value.
In order to silently replace an empty string (or null) with the value 'active' you will need a trigger:
create or replace function check_default()
returns trigger
as
$body$
begin
if (new.status is null or new.status = '') then
new.status = 'active';
end if;
return new;
end;
$body$
language plpgsql;
The above trigger would catch stuff like this:
insert into foo (id, active)
values (42,'');
update foo
set active = ''
where id = 42;
You could even extend that logic to also replace whitespace only values (' ') with the desired value.
Although it is possible to retrieve the default value dynamically in the trigger (to avoid having the same constant in two places) I would not do that for performance reasons.

How to programmatically check if row is deletable?

Say we have a PostgreSQL table like so:
CREATE TABLE master (
id INT PRIMARY KEY,
...
);
and many other tables referencing it with foreign keys:
CREATE TABLE other (
id INT PRIMARY KEY,
id_master INT NOT NULL,
...
CONSTRAINT other_id_master_fkey FOREIGN KEY (id_master)
REFERENCES master (id) ON DELETE RESTRICT
);
Is there a way to check (from within trigger function) if a master row is deletable without actually trying to delete it? The obvious way is to do a SELECT on all referencing tables one by one, but I would like to know if there is an easier way.
The reason I need this is that I have a table with hierarchical data in which any row can have child rows, and only child rows that are lowest in hierarchy can be referenced by other tables. So when a row is about to become a parent row, I need to check whether it is already referenced anywhere. If it is, it cannot become a parent row, and insertion of new child row is denied.
You can try to delete the row and roll back the effects. You wouldn't want to do that in a trigger function because any exception cancels all persisted changes to the database. The manual:
When an error is caught by an EXCEPTION clause, the local variables of
the PL/pgSQL function remain as they were when the error occurred, but
all changes to persistent database state within the block are rolled back.
Bold emphasis mine.
But you can wrap this into a separate block or a separate plpgsql function and catch the exception there to prevent the effect on the main (trigger) function.
CREATE OR REPLACE FUNCTION f_can_del(_id int)
RETURNS boolean AS
$func$
BEGIN
DELETE FROM master WHERE master_id = _id; -- DELETE is always rolled back
IF NOT FOUND THEN
RETURN NULL; -- ID not found, return NULL
END IF;
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
This returns TRUE / FALSE / NULL indicating that the row can be deleted / not be deleted / does not exist.
db<>fiddle here
Old sqlfiddle
One could easily make this function dynamic to test any table / column / value.
Since PostgreSQL 9.2 you can also report back which table was blocking.
PostgreSQL 9.3 or later offer more detailed information, yet.
Generic function for arbitrary table, column and type
Why did the attempt on a dynamic function that you posted in the comments fail? This quote from the manual should give a clue:
Note in particular that EXECUTE changes the output of GET DIAGNOSTICS, but does not change FOUND.
It works with GET DIAGNOSTICS:
CREATE OR REPLACE FUNCTION f_can_del(_tbl regclass, _col text, _id int)
RETURNS boolean AS
$func$
DECLARE
_ct int; -- to receive count of deleted rows
BEGIN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RETURN NULL; -- ID not found, return NULL
END IF;
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Old sqlfiddle
While being at it, I made it completely dynamic, including the data type of the column (it has to match the given column, of course). I am using the polymorphic type anyelement for that purpose. See:
How to write a function that returns text or integer values?
I also use format() and a parameter of type regclass to safeguard against SQLi. See:
SQL injection in Postgres functions vs prepared queries
You can do that also with Procedure.
CREATE OR REPLACE procedure p_delable(_tbl text, _col text, _id int)
AS $$
DECLARE
_ct bigint;
_exists boolean; -- to receive count of deleted rows
BEGIN
_exists := (SELECT EXISTS ( SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1 ));
IF _exists THEN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RAISE NOTICE 'no records found. no records will be deleted';
END IF;
ELSE
raise notice 'Input text is invalid table name.';
END IF;
EXCEPTION
WHEN undefined_column then
raise notice 'Input text is invalid column name.';
WHEN undefined_table then
raise notice 'Input text is invalid table name.';
WHEN FOREIGN_KEY_VIOLATION THEN
RAISE NOTICE 'foreign key violation, cannot be deleted.';
WHEN SQLSTATE 'MYERR' THEN
RAISE NOTICE 'rows % found and can be deleted.', _ct;
END
$$ LANGUAGE plpgsql;
You can call it, also can validate your input.
call p_delable('parent_tree', 'parent_id',30);
Will get:
NOTICE: no records found. no records will be deleted
Lets try an actual exist row.
call p_delable('parent_tree', 'parent_id',3);
It will return
NOTICE: rows 1 found and can be deleted.
It can also check your input table name exists in public schema or not.
call p_delable('parent_tre', 'parent_id',3);
It will give you notice:
NOTICE: Input text is invalid table name.