SQLite: Writing Trigger That Catches Insertions [duplicate] - sql

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.

Related

How to check if OLD column exist in Postgres Trigger Function

I want to create a deleted logs and insert data from the OLD row column. The problem is the column is not same for each table, some tables only has transaction_date and other table only has created_at. So I want to check if transaction_date just use it, otherwise use created_at column. I tried using coalesce function but still return:
ERROR: record "old" has no field "transaction_date" CONTEXT: SQL
statement "INSERT INTO "public"."delete_logs" ("table", "date") VALUES
(TG_TABLE_NAME, coalesce(OLD.transaction_date,
coalesce(OLD.created_at, now())))" PL/pgSQL function delete_table()
line 2 at SQL statement
here is my function:
CREATE OR REPLACE FUNCTION delete_table() RETURNS trigger AS
$$BEGIN
INSERT INTO "public"."deleted_logs" ("table", "created_at") VALUES (TG_TABLE_NAME, coalesce(OLD.transaction_date, coalesce(OLD.created_at, now())));
RETURN OLD;
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER "testDelete" AFTER DELETE ON "exampletable" FOR EACH ROW EXECUTE PROCEDURE "delete_table"();
Actually, I wanted to create a function for each table, but I think it will be difficult to update the function in the future, so I need to create a single function for all tables.
So I want to check if transaction_date just use it, otherwise use created_at column.
You can avoid the exception you saw by converting the row to json:
CREATE OR REPLACE FUNCTION log_ts_after_delete()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
INSERT INTO public.deleted_logs
(table_name , created_at) -- "table" is a reserved word
VALUES (TG_TABLE_NAME, COALESCE(to_json(OLD)->>'transaction_date', to_json(OLD)->>'created_at')::timestamptz);
RETURN NULL; -- not used in AFTER trugger
END
$func$;
My answer assumes that transaction_date is defined NOT NULL. Else, the expression defaults to created_at. Probably not what you want.
JSON is not as strict as SQL. A reference to a non-existing JSON key results in NULL instead of the exception for the reference to a non-existing table column. So COALESCE just works.
Related:
How to set value of composite variable field using dynamic SQL
If the row is wide, it might be cheaper to convert to JSON only once and save it to a variable, or do it in a subquery or CTE.
Related:
To convert from Python arrays to PostgreSQL quickly?
If tables never switch the columns in question, passing a parameter in the trigger definition would be much cheaper.
You find out (at trigger creation time) once with:
SELECT attname
FROM pg_attribute
WHERE attrelid = 'public.exampletable'::regclass
AND attname IN ('transaction_date', 'created_at')
AND NOT attisdropped
ORDER BY attname DESC
This returns 'transaction_date' if such a column exists in the table, else 'created_at', else NULL (no row). Related:
PostgreSQL rename a column only if it exists
It's still cheapest to have a separate trigger function for each type of trigger. Just two functions instead of one. If the trigger is fired often I would do that.
Avoid exception handling if you can. The manual:
Tip
A block containing an EXCEPTION clause is significantly more
expensive to enter and exit than a block without one. Therefore, don't
use EXCEPTION without need.

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.

How to disambiguate a plpgsql variable name in a ON CONFLICT clause?

Given this table:
create table test (
name text primary key
);
I need to write a plpgsql function with a variable name that collides with the primary key name, which I must use in a on conflict clause:
create or replace function func(
name text -- this variable name...
) returns void language plpgsql as
$$
begin
insert into test (name) values (name)
on conflict (name) do update -- ...conflicts with this line
set name = func.name;
end;
$$;
This compiles, but then throws an ambiguous column reference:
select * from func('one');
ERROR: column reference "name" is ambiguous
LINE 2: on conflict (name) do update
^
DETAIL: It could refer to either a PL/pgSQL variable or a table column.
QUERY: insert into test (name) values (name)
on conflict (name) do update
set name = func.name
CONTEXT: PL/pgSQL function func(text) line 3 at SQL statement
I tried specifying the full column name as on conflict (test.name) which does not compile, or ((test.name)) which compiles:
create or replace function func(
name text
) returns void language plpgsql as
$$
begin
insert into test (name) values (name)
on conflict ((test.name)) do -- this fails too
update set name = func.name;
end;
$$;
But it fails as well:
select * from func('two');
ERROR: invalid reference to FROM-clause entry for table "test"
LINE 2: on conflict ((test.name)) do
^
HINT: There is an entry for table "test", but it cannot be referenced from this part of the query.
QUERY: insert into test (name) values (name)
on conflict ((test.name)) do
update set name = func.name
CONTEXT: PL/pgSQL function func(text) line 3 at SQL statement
Is there a solution?
Edit: I found a workaround:
on conflict on constraint test_pkey do update
where test_pkey is the table name plus _pkey. I don't know how reliable this is though. I'd still like to specify the column name instead.
to start with, name is a bad name for both variable and attribute. When you have both, code won't look good. with that in mind, you can "prefix" variable with labeled block (in example below <<fn>>``), and setvariable_conflict` to give preference to column name, see code below:
t=# create or replace function func(
name text
) returns void language plpgsql as
$$
#variable_conflict use_column
<<fn>>
declare name text :='blah';
begin
insert into test (name) values (name)
on conflict (name) do -- this no longer fails
update set name = fn.name;
end;
$$;
t=# insert into test select 'b';
INSERT 0 1
Time: 8.076 ms
t=# select func('b');
func
------
(1 row)
Time: 6.117 ms
t=# select * from test;
name
------
b
blah
(2 rows)
https://www.postgresql.org/docs/current/static/plpgsql-implementation.html#PLPGSQL-VAR-SUBST
By default, PL/pgSQL will report an error if a name in a SQL statement
could refer to either a variable or a table column. You can fix such a
problem by renaming the variable or column, or by qualifying the
ambiguous reference, or by telling PL/pgSQL which interpretation to
prefer.
and further - basically the whole link is about it.
And yet - after demonstrating how particular task this can be easily done with plpgsql, I still quote namual:
The simplest solution is to rename the variable or column. A common
coding rule is to use a different naming convention for PL/pgSQL
variables than you use for column names. For example, if you
consistently name function variables v_something while none of your
column names start with v_, no conflicts will occur.
The ON CONFLICT... syntax (as documented here) uses a unique constraint to determine if the row conflicts. You can specify this unique constraint either by listing the columns it contains (at which point Postgres "infers" the correct index to use) or by naming the constraint directly.
In your case, the unique constraint being used is the primary key constraint implicitly created during your CREATE TABLE statement. This will have a name given to it by the DBMS, unless you specify one directly; so you will need to either look up the name the DBMS has given it (and be aware that this may change if you recreate the schema later), or name it explicitly when you create the table using the syntax CONSTRAINT pk_some_name PRIMARY KEY.
You would then specify the clause as ON CONFLICT ON CONSTRAINT pk_some_name DO ... (note no brackets around the constraint name).
(Alternatively, of course, you could change your function to use an unambiguous parameter name; personally, I think it's good practice to use a prefix like p_ or in_ rather than handling conflicts on a case-by-case basis.)

Postgresql BEFORE INSERT trigger not running before constraints are checked

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.

Disambiguating a function with INSERT INTO "table" FROM "table" RETURNING "id" = "id"

I'm having trouble disambiguating this particular postgres function that inserts on a FROM statement of the target table and returns the newly created unique id:
CREATE OR REPLACE FUNCTION "copyEntry"(OUT "entryId" INTEGER, IN "copyEntryId" INTEGER) RETURNING VOID AS $$
BEGIN
INSERT INTO "entries" ("data") SELECT "data" FROM "entries" WHERE "entryId" = "copyEntryId" RETURNING "entryId" INTO "entryId";
--Other plpgsql code below
END;
$$ LANGUAGE 'plpgsql';
"entryId" INTO "entryId" is ambiguous and I can't seem to find a way to alias the insert table to remove ambiguity. I would like to keep the output parameter to "entryId"
The ambiguity is between the variable and the column. There are two ways of disambiguating variable names: prefixing them based on table and code block names, or renaming them to be unique.
In this case, the parameters are declared in the outermost code block of the function, which is named after the function. So the "entryId" parameter can be referenced as "copyEntry"."entryId". Meanwhile, the column is from the table entries, so can be referenced as entries."entryId".
It may however be more readable to name your variables so that they aren't ambiguous in the first place, perhaps using a prefixing convention, so that your parameter would be "out_entryId".
Name the parameters to the function so you can distinguish them from columns in the table. This is a good programming practice.
Something like this:
CREATE OR REPLACE FUNCTION "copyEntry"(OUT "out_entryId" INTEGER, IN "in_copyEntryId" ) RETURNING VOID AS $$
BEGIN
INSERT INTO "entries"
SELECT "data"
FROM "entries" e
WHERE "entryId" = "in_copyentryid"
RETURNING "entryId" ;
--Other plpgsql code below
END;
The RETURNING clause should return the value from the row being inserted -- which is presumably some sort of default or auto-incremented value. As the documentation describes:
The optional RETURNING clause causes INSERT to compute and return
value(s) based on each row actually inserted. This is primarily useful
for obtaining values that were supplied by defaults, such as a serial
sequence number. However, any expression using the table's columns is
allowed. The syntax of the RETURNING list is identical to that of the
output list of SELECT.
The problem with your code might be the issue that the parameter has the same name as the function.