Postgresql BEFORE INSERT trigger not running before constraints are checked - sql

I have a trigger which is trimming some data on insert. One of the columns only holds 20 characters. If its 21 characters, we trim off the first character. However when I attempt to insert 21 characters, it response with an error that the value is too long. So it seems like its checking constraints before running my trigger?
Trigger creation:
CREATE TRIGGER my_trigger
BEFORE INSERT
ON public.mytable
FOR EACH ROW
EXECUTE PROCEDURE public.my_trigger();
The Trigger has some code in it like this:
IF (NEW.mydata IS NOT NULL AND LENGTH(NEW.mydata) > 20 THEN
NEW.mydata := substring(NEW.mydata from 2);
END IF;
However, when I attempt an insert and I insert 21 characters into it, it fails with:
[Err] ERROR: value too long for type character varying(20)

Postgres will not check table constraints before running the trigger. However, it guarantees that the types of the fields in NEW match the types of the fields in the table, and so it must check type constraints before giving you a chance to see the data.
You can work around this by removing the size limit from the type, and implementing your length check as a table constraint instead:
ALTER TABLE mytable
ALTER COLUMN mydata TYPE TEXT,
ADD CONSTRAINT mydata_length CHECK (length(mydata) <= 20);
Internally, TEXT and VARCHAR(N) are identical in Postgres, so the type change shouldn't have any impact as far as the database is concerned. However, these types are treated very differently by some client libraries, so be wary of differences in performance / behaviour in your application.

Related

How can i do an insert into a table of my DB that has one attribute of ADT type? with Oracle Live SQL

I have created a table for seeing how many people could die from COVID-19 in Latin country's for that i created an ADT structure which have two attributes probabilidad_fallecidos that means probability to death and cantidad_infectados that is the quantity of infected per country, The part i'm having problems is when i try to do an insert says ORA-00947: not enough values
I'm very new at this, this is my first try
Below i will let my ADT structure,my function, my table and my try of insert
ADT
CREATE OR REPLACE TYPE infectados AS OBJECT(
cantidad_infectados number,
probabilidad_fallecidos number,
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
);
Function cantidad_fallecidos
CREATE OR REPLACE TYPE BODY infectados IS
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
IS numero1 number(1);
BEGIN
IF cantidad_infectados > probabilidad_fallecidos*cantidad_infectados THEN
RETURN (probabilidad_fallecidos*cantidad_infectados);
ELSE
RAISE_APLICATION_ERROR(-2000,'Error: cantidad_infectados es menor a la probabilidad de fallecidos');
END IF;
END;
END;
Creation of my table
CREATE TABLE Vnzla_infectado(
vnzlaInf_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
num_infectados infectados
);
Try of insert
INSERT INTO Vnzla_infectado
VALUES (infectados(100,0.1,infectados.cantidad_fallecidos(100,0.1)));
You are getting the error ORA-00947: not enough values because you are supplying one value to insert into a table with two columns, and you are not specifying which column you are trying to insert into so Oracle thinks you are inserting into all columns.
Your vnzlaInf_id column may be generated by an IDENTITY, but it looked to the database as if you were attempting to insert a value into that column and nothing into the num_infectados column, hence the error about not enough values.
So the first thing you need to do is to modify the INSERT statement to tell the database which column you want to insert into:
INSERT INTO Vnzla_infectado (num_infectados)
VALUES ...
I wrote "first thing" because there is another problem with your INSERT statement. If you add that column name, you get another error, ORA-02315: incorrect number of arguments for default constructor. This is because your type constructor has two arguments, but you are specifying three. One way to fix it is to get rid of the third argument:
INSERT INTO Vnzla_infectado (num_infectados)
VALUES (infectados(100,0.1));
This INSERT statement runs successfully.
Alternatively, you may want to add another field to your type:
CREATE OR REPLACE TYPE infectados AS OBJECT(
cantidad_infectados number,
probabilidad_fallecidos number,
your_new_field_name_here number,
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
);
If you are going to change the type, you will have to drop the table first and recreate it afterwards. After doing this, your original INSERT statement runs fine.
While I'm here, there are some other problems I noticed with your static function cantidad_fallecidos. Firstly, there is a typo in RAISE_APLICATION_ERROR, it should be RAISE_APPLICATION_ERROR - you're missing one of the Ps. Secondly, the argument -2000 will get rejected by Oracle: it will complain with ORA-21000: error number argument to raise_application_error of -2000 is out of range if you attempt to raise your custom error. I guess you meant to use -20000 for the error number instead. Thirdly, the condition
cantidad_infectados > probabilidad_fallecidos*cantidad_infectados
looks a bit odd to me. Provided cantidad_fallecidos is greater than zero, then it is equivalent to
1 > probabilidad_fallecidos
Also, are you sure you need to use > rather than >=? This leads to some odd behaviour in unusual cases: if cantidad_infectados is zero, your condition will never be true and your custom error will be raised whatever probabilidad_fallecidos is. To me it makes more sense to validate that probabilidad_fallecidos is between 0 and 1.

SQLite: Writing Trigger That Catches Insertions [duplicate]

relevant documentation
I am trying to create a trigger that catches inserts into the Viewings table where the foreign key (viewings.location) does not correspond to an existing primary key in the Places table (places.location). The logic, from what I can tell, works as expected. However my issue comes from trying to concatenate the attempted value into the error-message in the raise function. Is this not allowed?
create trigger catchForeignKeyError BEFORE INSERT ON VIEWINGS
BEGIN
SELECT CASE
WHEN NEW.location NOT IN (SELECT PLACES.location FROM PLACES) THEN
RAISE(ABORT, 'Error: Insert into the VIEWINGS table references location '''||NEW.location||''' that is not found in the PLACES table.')
END;
END;
In the SQLite grammar, the second parameter of the RAISE() expression is not a string but a name:
RAISE(ABORT, some_error)
Identifiers can be quoted with double quotes, and for historical reasons, SQLite accepts a string (with single quotes) where an identifier is expected, but then it must be a single string, not a string expression composed of other values:
RAISE(ABORT, "some error")
There is no mechanism to get a dynamic value into the error message, except by creating a user-defined function for this.

Generic Postgres 9.5 trigger to convert an UPDATE into modified INSERT

Is it possible to create a generic (not table-specific) trigger in Postgres 9.5 that would perform on instead of update that converts the update into an insert?
Basically what I want to do is (pseudocode):
sql
instead of UPDATE on TG_TABLE_NAME INSERT on TG_TABLE_NAME
I know I can create a very table-specific trigger that maps each value into an insert statement. What I'm trying to do is get away from creating this trigger on every single table.
It is a bit of an oddball idea (nothing personal), but how about this:
CREATE FUNCTION not_update_but_insert() RETURNS trigger AS $$
BEGIN
INSERT INTO TG_TABLE_NAME -- Do an INSERT...
SELECT NEW.*; -- ... using the values from the row to be updated
RETURN NULL; -- Fail the UPDATE
END;
$$ LANGUAGE plpgsql;
Obviously this would not work for any table that has a PRIMARY KEY or other UNIQUE constraints. You do have to CREATE TRIGGER x BEFORE UPDATE for every table this would apply to, so analyze your table structure before creating the trigger.
There is obviously a work-around - at least for the PKs based on a sequence - by examining the information_schema for "safe" columns in TG_TABLE_NAME and then assembling their names into strings to splice into the INSERT statement (column list of main statement and select list). Just leave the columns with sequences or appropriate default values out. This, however, does not address UNIQUE constraints that have no obvious replacement (like a user name or an email address).

How to add constraint to sql table so that table has exactly one row

Parameter table is initially created and one row is added in Postgres.
This table should have always one row, otherwise SQL queries using this table will produce incorrect results. DELETE or INSERT to this table are disallowed, only UPDATE is allowed.
How to add single row constraint to this table?
Maybe DELETE and INSERT triggers can raise an exception or is there simpler way?
The following will create a table where you can only insert one single row. Any update of the id column will result in an error, as will any insert with a different value than 42. The actual id value doesn't matter actually (unless there is some special meaning that you need).
create table singleton
(
id integer not null primary key default 42,
parameter_1 text,
parameter_2 text,
constraint only_one_row check (id = 42)
);
insert into singleton values (default);
To prevent deletes you can use a rule:
create or replace rule ignore_delete
AS on delete to singleton
do instead nothing;
You could also use a rule to make insert do nothing as well if you want to make an insert "fail" silently. Without the rule, an insert would generate an error. If you want a delete to generate an error as well, you would need to create a trigger that simply raises an exception.
Edit
If you want an error to be thrown for inserts or deletes, you need a trigger for that:
create table singleton
(
id integer not null primary key,
parameter_1 text,
parameter_2 text
);
insert into singleton (id) values (42);
create or replace function raise_error()
returns trigger
as
$body$
begin
RAISE EXCEPTION 'No changes allowed';
end;
$body$
language plpgsql;
create trigger singleton_trg
before insert or delete on singleton
for each statement execute procedure raise_error();
Note that you have to insert the single row before you create the trigger, otherwise you can't insert that row.
This will only partially work for a superuser or the owner of the table. Both have the privilege to drop or disable the trigger. But that is the nature of a superuser - he can do anything.
To make any table a singleton just add this column:
just_me bool NOT NULL DEFAULT TRUE UNIQUE CHECK (just_me)
This allows exactly one row. Plus add the trigger #a_horse provided.
But I would rather use a function instead of the table for this purpose. Simpler and cheaper.
CREATE OR REPLACE FUNCTION one_row()
RETURNS TABLE (company_id int, company text) LANGUAGE sql IMMUTABLE AS
$$SELECT 123, 'The Company'$$
ALTER FUNCTION one_row() OWNER TO postgres;
Set the owner to the user that should be allowed to change it.
Give a user permission to ALTER a function
Nobody else change it - except superusers of course. Superusers can do anything.
You can use this function just like you would use the table:
SELECT * FROM one_row();
If you need a "table", create a view (which is actually a special table internally):
CREATE VIEW one_row AS SELECT * FROM one_row();
I guess you will not use the PostgreSQL root user in your application so you could simply limit the permissions of your application user on UPDATE for this table.
An INSERT or DELETE will then cause an Insufficient privilege exception.

Invalid NEW or OLD specification error

I'm trying to create trigger that checks if a phone number if in the (###) ###-#### format, if it this then nothing will happen, if is not then it will be fixed; however if there are more than 10 digits in the number then it will be turned to NULL.
Unfortunately I keep getting the Invalid NEW or OLD specification error in this trigger and I don't know why.
CREATE OR REPLACE TRIGGER phone_correction
BEFORE INSERT OR UPDATE OF vendor_phone
ON vendors
FOR EACH ROW
WHEN (NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
BEGIN
IF :NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\D*(?:\d\D*){10}$')
THEN
:NEW.vendor_phone := null;
DBMS_OUTPUT.PUT_LINE( 'The phone number is bad so setting to null.');
ELSE
:NEW.vendor_phone := REGEXP_LIKE(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$');
END IF;
END;
There are several errors here; as others have said you need to explicitly use :new. and :old. to reference columns in your trigger, so REGEXP_LIKE(vendor_phone becomes REGEXP_LIKE(:new.vendor_phone.
However, there are some more fundamental errors.
As with the LIKE operator, REGEXP_LIKE() returns a Boolean. Thus, your statement:
IF :NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\D*(?:\d\D*){10}$')
is actually IF <string> != <Boolean>, which'll never work.
Using DBMS_OUTPUT in a trigger isn't of any help to you unless you're going to be there to look at whatever logs you're keeping for every row that's been inserted, and then do something to correct whatever issues there are.
Silently removing data is bad practice, if you're going to change something then it's better to raise an error and let the calling code/user decide what to do instead.
If you don't want to let the calling code/user do anything and definitely want to NULL the column if it doesn't conform to a pattern then don't try and insert the data at all.
The ELSE condition in your IF statement is unnecessary, as :new.vendor_phone is already in the correct format.
Personally, I'd completely remove the trigger and add a constraint to check that the format in the column is the one in which you want:
SQL> alter table vendors
2 add constraint chk_vendors_phone
3 check (regexp_like(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'));
Then, when trying to insert data it'll be successful if the format is correct and unsuccessful if the format is incorrect:
SQL> insert into vendors (vendor_phone)
2 values ('(123) 123-1234');
1 row created.
SQL> insert into vendors (vendor_phone)
2 values ('(123) 123-124');
insert into vendors (vendor_phone)
*
ERROR at line 1:
ORA-02290: check constraint (CHK_VENDORS_PHONE) violated
SQL>
You can then decide what to do with the phones that have errored. As I've stated above, if you definitely want to NULL the incorrectly formatted phones then only insert data which matches this pattern. If anyone touches the code the check constraint will ensure that the data is still in the correct format.
If you absolutely must use a trigger, then it can be simplified to something like the following:
create or replace trigger phone_correction
before insert or update of vendor_phone
on vendors
for each row
when (not regexp_like(new.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
begin
:new.vendor_phone := null;
end;
This checks to see (using Boolean logic) whether the result of the REGEXP_LIKE() function is false. If it is, then it NULLs the phone. Here's an example of it working:
SQL> create table vendors (id number, vendor_phone varchar2(100));
Table created.
SQL> create trigger phone_correction
2 before insert or update of vendor_phone
3 on vendors
4 for each row
5 when (not regexp_like(new.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
6 begin
7 :new.vendor_phone := null;
8 end;
9 /
Trigger created.
SQL> insert into vendors
2 values (1, '(123) 123-1234');
1 row created.
SQL> insert into vendors
2 values (2, '(123) 123-124');
1 row created.
SQL> select * from vendors;
ID VENDOR_PHONE
---------- --------------------
1 (123) 123-1234
2
SQL>
... instead of setting a phone number to null :new.vendor_phone := null; how would you make so it can automatically modify the phone number into the correct format? (###) ###-####
This is actually the example in the documentation for REGEXP_REPLACE(). To make this more extensible, I'd remove all non-numeric characters from the string and then attempt the transformation. In order to remove the non-numeric characters:
regexp_replace(vendor_phone, '[^[:digit:]]')
This means replace everything that's not in the character class [:digit:] with nothing. Then, to transform you can use sub-expressions as described in the documentation:
regexp_replace(regexp_replace(vendor_phone, '[^[:digit:]]')
, '^([[:digit:]]{3})([[:digit:]]{3})([[:digit:]]{4})$'
, '(\1) \2-\3')
This looks for 3 ({3}) digits twice and then 4 digits, splitting them into sub-expressions and then putting them in the correct format. There are many ways to do this, and this may not be the quickest, but it makes your intention most clear.
I would not do this in a trigger, do this when you insert into the table instead. Better, and if this is a client-side application, you should be ensuring that your numbers are in the correct format before you hit the database at all.
You have to specify the :NEW whenever you are using the column names. try this:
CREATE OR REPLACE TRIGGER phone_correction
BEFORE INSERT OR UPDATE OF vendor_phone
ON vendors
FOR EACH ROW
WHEN (NEW.vendor_phone != REGEXP_LIKE(NEW.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
BEGIN
IF :NEW.vendor_phone != REGEXP_LIKE(:NEW.vendor_phone, '^\D*(?:\d\D*){10}$')
THEN
:NEW.vendor_phone := null;
DBMS_OUTPUT.PUT_LINE( 'The phone number is bad so setting to null.');
ELSE
:NEW.vendor_phone := REGEXP_LIKE(:NEW.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$');
END IF;
END;
You must specify the keywords NEW and OLD preceded by a colon (:) everywhere you are referring to the columns.
The only exception to this rule is the WHEN clause.
The NEW and OLD keywords, when specified in the WHEN clause, are not
considered bind variables, so are not preceded by a colon (:).
However, you must precede NEW and OLD with a colon in all references
other than the WHEN clause.
So, in your code, you must refer the new values in the conditions as :NEW.
REGEXP_LIKE(vendor_phone
Should be,
REGEXP_LIKE(:NEW.vendor_phone