Best practices for asserting some condition in SQL when creating a table? - sql

Imagine I create some table:
CREATE TABLE mytable AS
...
Now I want to conduct some sanity check, verify some condition is true for every record of mytable. I could frame this problem as checking whether the result of another query returns zero results.
SELECT count(*)
FROM mytable
WHERE something_horrible_is_true
Is there a standard, recommended way to generate an error here if the count is not equal to zero? To make something happen such that if I'm executing this sanity check query using a java.sql.Statement, a SQLException is triggered?
Is this a reasonable approach? Or is this a better way to enforce that some condition is always true when creating a table? (I use Postgresql.)

Create function to raise exception:
create or replace function raise_error(text) returns varchar as $body$
begin
raise exception '%', $1;
return null;
end; $body$ language plpgsql volatile;
Then you can use it in a regular SQLs:
SELECT case when count(*) > 0 then raise_error('something horrible is true!') end
FROM mytable
WHERE something_horrible_is_true
Here you will get the SQL exception if there are rows that satisfy the something_horrible_is_true condition.
There are also several more complex usage examples:
SELECT
case
when count(*) = 0 then raise_error('something horrible is true!')::int
else count(*)
end
FROM mytable
WHERE something_horrible_is_true
Returns count or rise exception when nothing found.
update mytable set
mydatefield =
case
when mydatefield = current_date then raise_error('Can not update today''s rows')::date
else '1812-10-10'::date
end;
Prevents to update some rows (this is a somewhat contrived example but it shows yet another usage way)
... and so on.

Are you familiar with triggers? Postresql provides good suport for triggers especially using the pgsql laguange.
A trigger is a function (check) that is always run on an event: insert, update,delete. You can call the function before or after the event.
I believe once you know this concept, you can find an online tutorial to help you achieve your goal.
A general approach may look like this:
CREATE FUNCTION trigger_function() RETURN trigger AS
$$
DECLARE c integer;
BEGIN
SELECT count(*) into c FROM mytable WHERE something_horrible_is_true;
IF c>0 then RAISE EXCEPTION 'cannot have a negative salary';
END IF;
return new;
END;
$$ LANGUAGE plpgsql;
And afterwards you execute
CREATE TRIGGER trigger_name BEFORE INSERT
ON table_name
FOR EACH ROW
EXECUTE PROCEDURE trigger_function()
Both code sections are pqsql.

Related

syntax Error in PostgreSQL when I try to create Trigger

I want to create trigger in PostgreSQL.
Logic is very simple.
I need trigger, if published_at updated and written_at is null, set published_at to written_at.
I wrote this one, but it failed. Does anybody have an idea?
CREATE function setWrittenAt() RETURNS trigger;
AS
DECLARE old_id INTEGER;
BEGIN ;
old_id = OLD.id
IF NEW.published_at IS NOT and NEW.written_at IS null
THEN
UPDATE review SET NEW.written_at = NEW.published_at where id = old_id;
END IF ;
RETURN NEW;
END;
LANGUAGE plpgsql;
CREATE TRIGGER update_written_at
AFTER UPDATE OF published_at ON review
WHEN (OLD.published_at IS DISTINCT FROM NEW.published_at)
EXECUTE PROCEDURE setWrittenAt();
Error:
Syntax error: 7 ERROR: syntax error at or near "DECLARE"
LINE 3: DECLARE old_id INTEGER;
There are multiple errors in your code:
IS NOT is not a valid expression you need IS NOT NULL.
After BEGIN and the returns clause there must be no ;
you forgot to enclose the function body as a string (which is easier to write if you use dollar quoting
you also don't need an unnecessary (additional) UPDATE if you make it a before trigger
CREATE function setwrittenat()
RETURNS trigger
AS
$$
BEGIN
IF NEW.published_at IS NOT NULL and NEW.written_at IS null THEN
NEW.written_at := = NEW.published_at; --<< simply assign the value
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Then use a BEFORE trigger:
CREATE TRIGGER update_written_at
BEFORE UPDATE OF published_at ON review
WHEN (OLD.published_at IS DISTINCT FROM NEW.published_at)
FOR EACH ROW
EXECUTE PROCEDURE setWrittenAt();
this is based on a_horse_with_no_names answer, since it'll throw an error.
ERROR: statement trigger's WHEN condition cannot reference column values
You need to add FOR EACH ROW, else conditional triggers will not function.
If neither is specified, FOR EACH STATEMENT is the default.
Statement-level triggers can also have WHEN conditions, although the feature is not so useful for them since the condition cannot refer to any values in the table.
See here
CREATE TRIGGER update_written_at
BEFORE UPDATE OF published_at ON review
FOR EACH ROW
WHEN (OLD.published_at IS DISTINCT FROM NEW.published_at)
EXECUTE PROCEDURE setWrittenAt();
I can not comment yet, which is why I've posted this as an answer.

Postgres Function not working when I have a large result

I'm copying information from table 1(tmp_subtype) to table 2(subtype_user). I have a test table 1 with 15 registers. I run this function into postgres:
CREATE OR REPLACE FUNCTION VERIFY_AND_INSERT_SUPTYPE()
RETURNS text AS $$
DECLARE
register_subtype RECORD;
existe INT DEFAULT 0;
MESSAGE_EXCEPTION TEXT;
cursor_subtype CURSOR
FOR
SELECT tsd.subtype,tsd.type_id_client,tsd.id_client,tsd.email
FROM tmp_subtype tsd;
BEGIN
OPEN cursor_subtype;
LOOP
FETCH cursor_subtype INTO register_subtype;
EXIT WHEN NOT FOUND;
SELECT COUNT(*) INTO existe FROM (
SELECT sdu.id_client FROM subtype_user sdu
WHERE sdu.id_client = register_subtype.id_client AND sdu.type_id_client = register_subtype.type_id_client
LIMIT 1
) SUB0;
IF existe = 0 THEN
INSERT INTO subtype_user(subtype,type_id_client,id_client,email)
VALUES (register_subtype.subtype,register_subtype.type_id_client,register_subtype.id_client,register_subtype.email);
ELSE
UPDATE subtype_user sdu2 SET subtype=register_subtype.subtype,email=register_subtype.email
WHERE sdu2.id_client = register_subtype.id_client AND sdu2.type_id_client = register_subtype.type_id_client;
END IF;
END LOOP;
CLOSE cursor_subtype;
RETURN 'OK';
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS MESSAGE_EXCEPTION = MESSAGE_TEXT;
RETURN MESSAGE_EXCEPTION;
END; $$
LANGUAGE plpgsql;
It works, but When I run this function with the real table 1, it is not working. The function finishes but nothing happend. The real table 1 has 1 million of registers.
Row-by-row processing with embedded counting is a recipe for slow and inefficient processing. Additionally your check for existence won't work if the function is invoked from concurrent transactions. As far as I can tell you can replace the whole loop and cursor with a single INSERT statement:
CREATE OR REPLACE FUNCTION VERIFY_AND_INSERT_SUPTYPE()
RETURNS text
AS $$
DECLARE
MESSAGE_EXCEPTION TEXT;
BEGIN
INSERT INTO subtype_user(subtype, type_id_client, id_client, email)
SELECT tsd.subtype, tsd.type_id_client, tsd.id_client, tsd.email
FROM tmp_subtype tsd
ON conflict (id_client, type_id_client) DO UPDATE
SET subtype = excluded.register_subtype,
email = excluded.email;
RETURN 'OK';
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS MESSAGE_EXCEPTION = MESSAGE_TEXT;
RETURN MESSAGE_EXCEPTION;
END; $$
LANGUAGE plpgsql;
I probable would not add an exception handler to begin with, so that the caller sees the complete exception.
It is hard to say, what is wrong on this code - in this situation RAISE NOTICE is your best friend. I see some issues in your code, but these issues are related to performance. Table with 1 mil rows is nothing.
the code in ISAM programming style can be really slow - instead cycle over cursor use INSERT ON CONFLICT .. statement.
SELECT COUNT(*) ... can be rewritten to little bit faster, but surely more readable form:
IF EXISTS(SELECT ... FROM subtype_user) THEN
UPDATE ...
ELSE
INSERT ...
END IF;
Handling errors from your example is little bit obsolete - catch only exception that you can really solve. Your type of exception handling doesn't solve any, and more, you lose details info about the exception (position, line, ...). Just don't do it.

SQL RAISE EXCEPTION not working

I'm not very good with SQL and I have a small problem with my code.
CREATE OR REPLACE FUNCTION cancelBooking() RETURNS TRIGGER AS $cancelBooking$
BEGIN
IF (NEW.bookingid not in(SELECT bookingid FROM flightbooking)) THEN
RAISE EXCEPTION 'ID NOT FOUND';
END IF;
END;
$cancelBooking$ LANGUAGE plpgsql;
CREATE TRIGGER cancelBooking BEFORE UPDATE ON flightbooking
FOR EACH ROW EXECUTE PROCEDURE cancelBooking();
UPDATE flightbooking
SET status = 'C'
WHERE bookingid=11;
After I update flightbooking with non existing ID it still says UPDATE 0 which didn't do anything of course but I want it to be an error not successfull query.
Any ideas? I tried to look for a solution on the internet but it didn't help.
Obviously the stated question is why it is not working (which is due to the problem discussed in the other answers. Obviously this will never work since the only case of the trigger being fired never can have be in a snapshot where a row with the same bookingid as NEW will be visible in the same snapshot.
Also I am not 100% sure but I am worried about performance in your function. (PLPGSQL is a bit funny at times).
Try this instead as it is clearer what is going on under the hood and therefore makes clearer what can be optimized.
CREATE OR REPLACE FUNCTION cancelBooking() RETURNS TRIGGER AS
$cancelBooking$
BEGIN
PERFORM * FROM flightbooking WHERE bookingid = NEW.bookingid;
IF NOT FOUND THEN
RAISE EXCEPTION 'ID NOT FOUND';
END IF;
END;
$cancelBooking$ LANGUAGE plpgsql;
I am guessing in most cases that that the performance difference will be very minimal but the performance implications and caveats are clearer so the opportunities to shoot yourself in the foot are less.
On to a real solution rather than a critique and diagnosis
This will never work as you have done it. You could do as follows instead:
CREATE OR REPLACE FUNCTION cancelBooling(_bookingid int) returns void
LANGUAGE PLPGSQL AS
$$
BEGIN
DELETE FROM flightbooking WHERE bookingid = _bookingid;
IF NOT FOUND THEN
RAISE EXCEPTION 'NOT FOUND';
END IF;
END;
$$;
Your trigger fires for each row being updated. Because there are no rows to update (the WHERE clause in the UPDATE doesn't find any), the trigger is never fired.

PostgreSQL 1 to many trigger procedure

I wrote this query in PostgreSQL:
CREATE OR REPLACE FUNCTION pippo() RETURNS TRIGGER AS $$
BEGIN
CHECK (NOT EXISTS (SELECT * FROM padre WHERE cod_fis NOT IN (SELECT padre FROM paternita)));
END;
$$ LANGUAGE plpgsql;
It returns:
Syntax error at or near CHECK.
I wrote this code because I have to realize a 1..n link between two tables.
You can't use CHECK here. CHECK is for table and column constraints.
Two further notes:
If this is supposed to be a statement level constraint trigger, I'm guessing you're actually looking for IF ... THEN RAISE EXCEPTION 'message'; END IF;
(If not, you may want to expand and clarify what you're trying to do.)
The function should return NEW, OLD or NULL.

Date Subtraction on PostgreSQL

`I have a table (workers) which has a startdate column(s_date) representing when employee started working. So i want to create a trigger that if it's less than a year (365 days) of working, i m gonna give an exception. But something's wrong with the code. Any help_?
CREATE OR REPLACE FUNCTION control_func() RETURNS TRIGGER AS '
declare
int1 integer;
tt Date;
begin
select now()::date into tt;
select s_date from workers;
if(tt-s_date<365) then
RAISE EXCEPTION ''A message'';
end if;
RETURN NULL;
END;
' LANGUAGE 'plpgsql';
You have several problems. First of all, don't use single quotes to quote function bodies, that just makes a big mess, use dollar quoting instead:
create or replace function f() returns trigger as $$
...
$$ language plpgsql;
Next, this doesn't do anything useful:
select s_date from workers;
That will try to grab all s_date values from workers and then throw them all away. You want to look at the current row for the trigger and that's available in NEW:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is NULL in statement-level triggers and for DELETE operations.
So you can look at new.s_date to see the date you're interested in:
select now()::date into tt;
if tt - new.s_date < 365 then
raise exception 'A message';
end if;
This is probably a row-level before insert or update trigger so you don't want return null; here; from the fine manual:
A row-level trigger fired before an operation has the following choices:
It can return NULL to skip the operation for the current row. This instructs the executor to not perform the row-level operation that invoked the trigger (the insertion, modification, or deletion of a particular table row).
For row-level INSERT and UPDATE triggers only, the returned row becomes the row that will be inserted or will replace the row being updated. This allows the trigger function to modify the row being inserted or updated.
So your return null; means "skip this INSERT or UPDATE if the new record is valid" and that's not what you want. You want to return new;.
You also have an unused variable. And you can use current_date instead of your tt.
Your function should look more like this:
create or replace function control_func() returns trigger as $$
begin
if current_date - new.s_date < 365 then
raise exception 'A message';
end if;
return new;
end;
$$ language plpgsql;
EDIT: the initial syntax is supposed to be datediff. However it is a MYSQL syntax. So you are better of with the above detailed and correct answer. ;-)
Ty this this in you code,
Add another variable to save s_date say ss,
If Datediff(days, tt, ss) < 365 then