tSQLt, triggers and testing - triggers

I have tried to wrap my brain around this, but can't make it work, so I present a little testcase here, and hopefully someone can explain it to me:
First a little test database:
CREATE DATABASE test;
USE test;
CREATE TABLE testA (nr INT)
GO
CREATE TRIGGER triggerTestA
ON testA
FOR INSERT AS BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT nr FROM Inserted WHERE nr > 10)
RAISERROR('Too high number!', 16, 1);
END;
And here is a tSQL test, to test the behaviour:
ALTER PROCEDURE [mytests].[test1] AS
BEGIN
EXEC tSQLt.FakeTable #TableName = N'testA'
EXEC tSQLt.ApplyTrigger
#TableName = N'testA',
#TriggerName ='triggerTestA'
EXEC tSQLt.ExpectException
INSERT INTO dbo.testA VALUES (12)
END;
This test will run ok - but the trigger doesn't do what I want: prevent user from entering values > 10. This version of the trigger does what I want:
CREATE TRIGGER triggerTestA
ON testA FOR INSERT AS BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
IF EXISTS (SELECT nr FROM Inserted WHERE nr > 10)
RAISERROR('Too high number!', 16, 1);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END;
But now the test fails, stating A) there is an error (which was expected!) and B) that there is no BEGIN TRANSACTION to match a ROLLBACK TRANSACTION. I guess this last error is with the tSQLt surrounding transaction, and that my trigger somehow interferes with that, but it is sure not what I expect.
Could someone explain, and maybe help me do it right?

tSQLt is currently restricted to run tests in its own transaction and reacts, as you have seen, unwelcoming when you fiddle with its transaction.
So, to make this test work, you need to skip the rollback within the test but not outside.
I suggest this approach:
Remove all transaction handling statements from the trigger. You don't need to begin a transaction anyway as triggers are always executed inside of one.
If you find a violating row, call a procedure that does the rollback and the raiserror
Spy that procedure for your test
To test that procedure itself, you could use tSQLt.NewConnection

Related

PL/pgSQL procedures and transaction control

I'm new to Postgres, but with experience from Oracle. Trying to create a stored procedure which is going to:
Insert a row
Handle exceptions and in case of an exception insert a row into a log table by calling dedicated procedure
Emit an audit log record into a log table in case the whole procedure ran successfully
By pseudo code:
CREATE OR REPLACE PROCEDURE test.p_insert(IN p_test_param character varying)
LANGUAGE 'plpgsql'
SECURITY DEFINER
AS $BODY$
DECLARE
-- some declarations
BEGIN
BEGIN
INSERT INTO test.a(a) VALUES (p_test_param);
EXCEPTION
WHEN OTHERS THEN
-- GET STACKED DIAGNOSTICS
CALL test.p_insert_log(...); -- Inserts a row into a log table, another COMMIT may be required?
RAISE;
END;
COMMIT; -- CAN'T DO
BEGIN
IF (SELECT test.f_debug()) THEN
CALL test.p_insert_log(...); -- Audit the execution
END IF;
END;
COMMIT; -- CAN'T DO EITHER
END;
$$BODY$$;
However when I try to test the procedure out from an anonymous block in PgAdmin such as:
BEGIN;
DO
LANGUAGE plpgsql
$$
BEGIN
CALL test.p_insert(
p_test_param => 'test'
);
END;
$$
I'm getting an error ERROR: invalid transaction termination. How can I get rid of it? My objective is to let the procedure carry out the transaction control, I don't want the caller to COMMIT or ROLLBACK anything. If I remove both COMMIT commands from the code of the procedure, it executes well, however the invoker must explicitly COMMIT or REVOKE the transaction afterwards, which is not desired. In Oracle the pseudo code with COMMIT statements would work, in Postgres it doesn't seem to work as I would like to. Could you please help me out? Thanks
Your code will work as intended. Perhaps you made some mistake in calling the code:
you cannot call the procedure from a function
you cannot call the procedure in an explicitly started transaction:
BEGIN;
CALL p_insert('something); -- will fail
COMMIT;

In Postgres, how do I write to a table in a child procedure during an error that is to be rolled back?

Hopefully I'm just missing something here, but I would like to be able to do the following:
call a procedure from another procedure.
the parent procedure has a rollback in an exception handler.
the child has an exception handler as well to conceal the inner error from the parent.
the child writes to a table in the error handler.
the child RAISES a new error in the error handler.
the parent catches this error and issues a rollback.
the child procedure's write to a table is preserved.
Possible?
I wrote the below test showing my issue. Please execute the segments separate. Thank you so much ahead of time.
create schema dbo;
drop table dbo.test_transaction_table;
drop table dbo.test_transaction_errortable;
create table dbo.test_transaction_table(a int);
create table dbo.test_transaction_errortable(a int);
/**********************************************************/
create or replace PROCEDURE dbo.test_transaction()
LANGUAGE plpgsql
AS $$
DECLARE v_returnvalue int;
BEGIN
insert into dbo.test_transaction_table(a) values(1);
call dbo.test_transaction_inner(v_returnvalue);
insert into dbo.test_transaction_table(a) values(3);
commit;
EXCEPTION WHEN OTHERS THEN
rollback;
END; $$;
create or replace PROCEDURE dbo.test_transaction_inner(INOUT v_returnvalue INTEGER)
LANGUAGE plpgsql
AS $$
BEGIN
insert into dbo.test_transaction_table(a) values(2);
v_returnvalue = 1 / 0;
v_returnvalue = 3;
EXCEPTION WHEN OTHERS THEN
--I WANT THIS BELOW INSERT TO ALWAYS HAPPEN
--I WANT TO RAISE AN ERROR SO THE PARENT PROCEDURE CAN STILL DO A ROLLBACK IF NECESSARY
insert into dbo.test_transaction_errortable(a) values(1);
COMMIT;
RAISE 'There was one or more errors.';
END; $$;
/**********************************************************/
delete from dbo.test_transaction_table;
delete from dbo.test_transaction_errortable;
call dbo.test_transaction();
select * from dbo.test_transaction_table;
select * from dbo.test_transaction_errortable;
/***********************************************************/
--I expect the dbo.test_transaction_errortable to have 1 row.
--I expect the dbo.test_transaction_table to have 0 rows.

How to prevent or avoid running update and delete statements without where clauses in PostgreSQL

How to prevent or avoid running update or delete statements without where clauses in PostgreSQL?
Same as SQL_SAFE_UPDATES statement in MySQL is needed for PostgreSQL.
For example:
UPDATE table_name SET active=1; -- Prevent this statement or throw error message.
UPDATE table_name SET active=1 WHERE id=1; -- This is allowed
My company database has many users with insert and update privilege any one of the users do that unsafe update.
In this secoario how to handle this.
Any idea can write trigger or any extension to handle the unsafe update in PostgreSQL.
I have switched off autocommits to avoid these errors. So I always have a transaction that I can roll back. All you have to do is modify .psqlrc:
\set AUTOCOMMIT off
\echo AUTOCOMMIT = :AUTOCOMMIT
\set PROMPT1 '%[%033[32m%]%/%[%033[0m%]%R%[%033[1;32;40m%]%x%[%033[0m%]%# '
\set PROMPT2 '%[%033[32m%]%/%[%033[0m%]%R%[%033[1;32;40m%]%x%[%033[0m%]%# '
\set PROMPT3 '>> '
You don't have to insert the PROMPT statements. But they are helpful because they change the psql prompt to show the transaction status.
Another advantage of this approach is that it gives you a chance to prevent any erroneous changes.
Example (psql):
database=# SELECT * FROM my_table; -- implicit start transaction; see prompt
-- output result
database*# UPDATE my_table SET my_column = 1; -- missed where clause
UPDATE 525125 -- Oh, no!
database*# ROLLBACK; -- Puh! revert wrong changes
ROLLBACK
database=# -- I'm completely operational and all of my circuits working perfectly
There actually was a discussion on the hackers list about this very feature. It had a mixed reception, but might have been accepted if the author had persisted.
As it is, the best you can do is a statement level trigger that bleats if you modify too many rows:
CREATE TABLE deleteme
AS SELECT i FROM generate_series(1, 1000) AS i;
CREATE FUNCTION stop_mass_deletes() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
IF (SELECT count(*) FROM OLD) > TG_ARGV[0]::bigint THEN
RAISE EXCEPTION 'must not modify more than % rows', TG_ARGV[0];
END IF;
RETURN NULL;
END;$$;
CREATE TRIGGER stop_mass_deletes AFTER DELETE ON deleteme
REFERENCING OLD TABLE AS old FOR EACH STATEMENT
EXECUTE FUNCTION stop_mass_deletes(10);
DELETE FROM deleteme WHERE i < 100;
ERROR: must not modify more than 10 rows
CONTEXT: PL/pgSQL function stop_mass_deletes() line 1 at RAISE
DELETE FROM deleteme WHERE i < 10;
DELETE 9
This will have a certain performance impact on deletes.
This works from v10 on, when transition tables were introduced.
If you can afford making it a little less convinient for your users, you might try revoking UPDATE privilege for all "standard" users and creating a stored procedure like this:
CREATE FUNCTION update(table_name, col_name, new_value, condition) RETURNS void
/*
Check if condition is acceptable, create and run UPDATE statement
*/
LANGUAGE plpgsql SECURITY DEFINER
Because of SECURITY DEFINER this way your users will be able to UPDATE despite not having UPDATE privilege.
I'm not sure if this is a good approach, but this way you can force as strict UPDATE (or anything else) requirements as you wish.
Of course the more complicated UPDATES are required, the more complicated has to be your procedure, but if this is mostly just about updating single row by ID (as in your example) this might be worth a try.

how to use COMMIT and ROLLBACK in a PostgreSQL function

I am using three insert statements, and if there is an error in the third statement, I want to rollback the first and the second one. If there is no way to do this, please tell me a different approach to handle this in PostgresqQL.
If I use COMMIT or ROLLBACK, I get an error.
CREATE OR REPLACE FUNCTION TEST1 ()
RETURNS VOID
LANGUAGE 'plpgsql'
AS $$
BEGIN
INSERT INTO table1 VALUES (1);
INSERT INTO table1 VALUES (2);
INSERT INTO table1 VALUES ('A');
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;$$;
The above code is not working; COMMIT and ROLLBACK are not supported by PostgreSQL functions.
You cannot use transaction statements like SAVEPOINT, COMMIT or ROLLBACK in a function. The documentation says:
In procedures invoked by the CALL command as well as in anonymous code blocks (DO command), it is possible to end transactions using the commands COMMIT and ROLLBACK.
Ex negativo, since functions are not procedures that are invoked with CALL, you cannot do that in functions.
The BEGIN that starts a block in PL/pgSQL is different from the SQL statement BEGIN that starts a transaction.
Just remove the COMMIT from your function, and you have the solution: since the whole function is always run inside a single transaction, any error in the third statement will lead to a ROLLBACK that also undoes the first two statements.
Compared to other SQL languages, you should think that Postgres always takes care of the commit/rollback in case of error implicitly when you are inside a transaction.
Here is what the doc is saying:
Transactions are a fundamental concept of all database systems. The essential point of a transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all.
CREATE OR REPLACE FUNCTION TEST1 ()
RETURNS VOID
LANGUAGE 'plpgsql'
AS $$
BEGIN
INSERT INTO table1 VALUES (1);
INSERT INTO table1 VALUES (2);
INSERT INTO table1 VALUES ('A');
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;$$;
For transaction control we use PROCEDURE (From postgresql11) instead of FUCTION.
FUNCTION does not support transaction inside the function. This is the main difference between FUNCTION and PROCEDURE in PostgreSQL.
Your code should be:
CREATE OR REPLACE PROCEDURE TEST1 ()
RETURNS VOID
LANGUAGE 'plpgsql'
AS $$
BEGIN
INSERT INTO table1 VALUES (1);
INSERT INTO table1 VALUES (2);
INSERT INTO table1 VALUES ('A');
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;$$;

How to continue executing rest of while loop even if an exception occurs in sybase?

This is related to this question but slightly different, I have while loop that inserts records and I want it to continue even if some inserts fail. So, the insertrecords procedure inserts records, by doing a where on the temp table for top 50 rows at a time.
The problem is that it won't continue if any of the inserts inside the insertrecords fail? How can I modify the sql to continue with the next 50 rows, even if it fails for current 50 records. I guess is there something like try/catch exception handling in sybase?
SELECT id INTO #temp FROM myTable
-- Loop through the rows of the temp table
WHILE EXISTS(SELECT 1 FROM #temp)
BEGIN
BEGIN TRANSACTION
exec insertrecords
IF ##error = 0
begin
print 'commited'
commit
end
else
begin
print 'rolled back'
rollback
end
DELETE TOP 50 FROM #temp order by id
END
-- Drop the temp table.
DROP TABLE #temp
Try putting the content inside your while block inside try cactch.
NOTE: The below sample is in SQL, try similar code in sybase.
`WHILE(SOME CONDITION)
BEGIN --start of while block
BEGIN TRY-start of try block
--your code here
END TRY
BEGIN CATCH
PRINT ERR_MESSAGE();
END CATCH
END --end of while loop.
`