what is the statement for "where" condtion in pl/sql - sql

I want to make a trigger that will be executed for each row before delete on the table Clients, what I'm trying to do is when some one tries to delete a row :
DELETE FROM Clients WHERE id=5 AND name = 'test';
I want to print something on the screen like :
"you deleted with the following conditions : id is 5 , name is test "
or execute another query with same conditions ...
Any kind of help is very appreciated
EDIT
let's suppose the user typed the following query : DELETE FROM Clients where name = 'test'
create or replace
TRIGGER DELETECLIENT
BEFORE DELETE ON CLIENTS
DECLARE
pragma autonomous_transaction;
name1 clients.name%Type;
BEGIN
select name into name1 from clients where name = %I want here the name in the user query %;
IF name1 != null THEN
DELETE FROM clients WHERE name = name1;
commit;
END IF;
END;
what I've tested :
i tried adding :old.name , and :new.name , but this doesn't work when the the row doesn't exist in my database
PS : i'm doing this for educational purpose only, I know the script doesn't make sense. i just wan't to know how to achieve the following task : getting the parameters typed after WHERE in the query

The correlation names and pseudorecords the represent, i.e. :old and :new by default, only have any meaning for row-level triggers:
Note:
This topic applies only to triggers that fire at row level—that is, row-level simple DML triggers and compound DML triggers with row-level timing point sections.
You cannot refer to these in a statement-level trigger, and only :old would have any meaning in a delete trigger. On the other hand, statement-level triggers fire even if no data is affected, whereas a row-level trigger won't fire if, in your example, no data is deleted - because there are no matching rows in your table. (This applies for instead of triggers for views as well, in case you're wondering if that would be a workaround).
So basically you can't do what you're attempting - conditionally deciding whether to delete from another table instead - from a trigger. Triggers are rarely the right answer. For your 'scenario where if it doesn't exist on my table i should try to find it on other site and delete it' you can try to delete a row locally, test whether it did anything, and then decide to delete from the remote table if it didn't. In a PL/SQL block that might be as simple as:
create procedure delete_client(p_name clients.name%type) as
begin
delete from clients where name = p_name;
if sql%rowcount = 0 then
delete from clients#site2 where name = p_name;
end if;
end delete_client;
/
And rather than deleting directly from the table, you make everyone call the procedure instead. Say you start off with some data:
select * from clients;
ID NAME
---------- ----------
1 Joe
2 Anna
select * from clients#site2;
ID NAME
---------- ----------
1 Joe
3 Max
Then calling the procedure for two names:
exec delete_client('Joe');
exec delete_client('Max');
... has removed local and remote records appropriately:
select * from clients;
ID NAME
---------- ----------
2 Anna
select * from clients#site2;
ID NAME
---------- ----------
1 Joe
Joe was only deleted from the local schema despite existing in both; Max didn't exist locally so was deleted from the remote schema.
It doesn't have to be a procedure; if you're deleting through JDBC etc. you can test the result of an execute() call to see how many rows were affected, which is all sql%rowcount is doing really, and the application code can decide whether to do the second delete.
But with a procedure (probably in a package) you can grant execute on that, and remove delete privileges from the users, so they can't bypass this check and do a straight delete from clients where ...
If you really want some 'display' output for testing purposes:
create procedure delete_client(p_name clients.name%type) as
begin
delete from clients where name = p_name;
if sql%rowcount > 0 then
dbms_output.put_line('Deleted ' || sql%rowcount
|| ' rows from local schema for "' || p_name || '"');
else
delete from clients#site2 where name = p_name;
if sql%rowcount > 0 then
dbms_output.put_line('Deleted ' || sql%rowcount
|| ' rows from remote schema for "' || p_name || '"');
else
dbms_output.put_line('No rows deleted on local or remote schema for "'
|| p_name || '"');
end if;
end if;
end delete_client;
/
set serveroutput on
exec delete_client('Joe');
anonymous block completed
Deleted 1 rows from local schema for "Joe"
exec delete_client('Max');
anonymous block completed
Deleted 1 rows from remote schema for "Max"
exec delete_client('Fred');
anonymous block completed
No rows deleted on local or remote schema for "Fred"
But you shouldn't assume anyone calling your procedure will have serveroutput on, or even be using a client capable of consuming dbms_output messages.

Related

How to Move a Stored Procedure in Oracle SQL from one Schema to Another?

I am trying to understand what steps I need to undertake in order to move a stored procedure from one schema and into another. The schema that this is currently sitting in is going to be made redundant and I have been asked to move all tables and procedures. I have no trouble with tables but never done anything with procedures hence want to make sure I don't miss anything out.
What I have currently done is look through the procedure and made a list of what its actually doing i.e. dropping/creating and inserting data into tables.
After this I wasn't sure if it was just a case of copying the procedure code and then creating a new procedure on the new schema with the same code and then compiling it.
I would really appreciate it if somebody could advise if I am missing anything in the steps that I am undertaking just to ensure I don't mess things up.
There is no way to "move" an object from one schema to another.
The only practible way I see here is copying the source code and then executing it in the new schema. As #pmdba wrote as comment, you should watch out for schema names like "MYSCHEMA"."TABLENAME" and other references.
If you got too much to copy you may consider writing a block where you automatically read the data of the old schema and create it automatically in the new one.
You can get the data of (nearly) everything with, i.e. procedures:
select * from all_source where owner = 'OLDSCHEMANAME' and type = 'PROCEDURE';
and use it like this:
begin
....
select listagg(text, '') within group (order by line) into proc_code
from all_source
where owner = 'OLDSCHEMANAME'
and type = 'PROCEDURE'
group by name;
execute immediate 'create or replace ' || proc_code; -- perhaps you need to remove the last ';' here
...
end;
Please note that this code is only meant as hint and doesn't need to be taken exactly that way. Also, you may still get errors due to non existing objects, wrong schema references etc..
To get the ddl of a table one may use select dbms_metadata.get_ddl('TABLE','Table_name','Schema_Name') from dual;.
By googling dbms_metadata.get_ddl you might get more info on the DBMS_METADATA-package and how to use it correctly.
As already stated, there is no mechanism to copy one object ( procedure , function or package, etc ) to another schema. One alternative is using all_source, but I prefer DBMS_METADATA because allows you to transfer all dependencies, like for example privileges. Imagine I need to copy a procedure but I need to keep the privileges, with this package I can get everything.
Example
SQL> create procedure myschema1.my_procedure ( p1 number )
2 as
3 var1 number := p1;
4 begin
5 select 1 into var1 from dual;
6 end;
7 /
Procedure created.
SQL> grant execute on myschema1.my_procedure to myuser ;
Grant succeeded.
Now, let's imagine we want to copy the procedure and its privileges to another schema
SQL> set long 99999999 set lines 200 pages 400
SQL> select dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1') from dual ;
DBMS_METADATA.GET_DDL('PROCEDURE','MY_PROCEDURE','MYSCHEMA1')
--------------------------------------------------------------------------------
CREATE OR REPLACE EDITIONABLE PROCEDURE "MYSCHEMA1"."MY_PROCEDURE" ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
But, imagine you don't want quotation and neither the editionable argument
SQL> select
replace(dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1','11.2.0'),'"','') as ddl from dual ;
DDL
--------------------------------------------------------------------------------
CREATE OR REPLACE PROCEDURE MYSCHEMA1.MY_PROCEDURE ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
Then to get the final command with the new schema owner, we use regexp_replace to replace the first occurrence
SQL> select regexp_replace(replace(dbms_metadata.get_ddl('PROCEDURE','MY_PROCEDURE','MYSCHEMA1','11.2.0'),'"',''),'MYSCHEMA1','MYSCHEMA2',1,1)
2 as ddl from dual ;
DDL
--------------------------------------------------------------------------------
CREATE OR REPLACE PROCEDURE MYSCHEMA2.MY_PROCEDURE ( p1 number )
as
var1 number := p1;
begin
select 1 into var1 from dual;
end;
Finally, we can get all privileges by
SQL> select dbms_metadata.get_dependent_ddl( 'OBJECT_GRANT' , 'MY_PROCEDURE' , 'MYSCHEMA1' ) from dual ;
DBMS_METADATA.GET_DEPENDENT_DDL('OBJECT_GRANT','MY_PROCEDURE','MYSCHEMA1')
--------------------------------------------------------------------------------
GRANT EXECUTE ON "MYSCHEMA1"."MY_PROCEDURE" TO "MYUSER"
Remember to apply at session level before to start some settings to enhance dbms_metadata output:
begin
DBMS_METADATA.set_transform_param (DBMS_METADATA.session_transform, 'SQLTERMINATOR', true);
DBMS_METADATA.set_transform_param (DBMS_METADATA.session_transform, 'PRETTY', true);
end;

Oracle delete rows in multiple tables based on single primary key.

There is about 20 tables which branch based on a single primary key - EmployeeId. There's about 12,000 employees I want completely gone from my database. The chances of other processes updating these employees when I am deleting them are close to zero. I am planning on deleting them in bulk and then committing. All the delete shouldn't ideally fail but I am unsure whether to go the cursor route, commit every 500 rows or something. Here's how it looks like now.
--STEP 1: Collect the EmployeeIds to delete in a temp table
Create table temp as select EmployeeId from Employee where <all conditions are met>;
--STEP 2: Delete names
Delete from EmployeeName where EmployeeId in (select EmployeeId from temp);
--STEP 3 - STEP 30: Delete all other child tables
Delete from table inner join another_table on some_key inner join yet_another_table on some_key where EmployeeId in (select EmployeeId from temp);
--STEP 4: Commit
commit;
If you're going to do this often, how about letting Oracle do the job for you ?
Set all your foreign keys referencing table Employee to "ON DELETE CASCADE" (see this link for example)
delete from Employee where <all your conditions>;
The FKs being set to "ON DELETE CASCADE", Oracle will automatically delete orphaned rows from child tables when a row is deleted in the parent table.
Assuming you want to maintain the integrity of the data and when there is an error deleting from one table then you want to ROLLBACK all the deletes for that employee then you could do something like:
DECLARE
TYPE Emp_ID_Tab_Type IS TABLE OF Employee.EmployeeID%TYPE;
All_Employees Emp_ID_Tab_Type;
Deleted_Employees Emp_ID_Tab_Type := Emp_ID_Tab_Type();
Error_Employees Emp_ID_Tab_Type := Emp_ID_Tab_Type();
BEGIN
SELECT EmployeeID
BULK COLLECT INTO All_Employees
FROM Employees
WHERE 1 = 0; -- Your conditions
FOR i IN 1 .. All_Employees.COUNT LOOP
BEGIN
DELETE FROM child_table1
WHERE EmployeeID = All_Employees(i);
DELETE FROM child_table2
WHERE EmployeeID = All_Employees(i);
-- ...
DELETE FROM child_table20
WHERE EmployeeID = All_Employees(i);
DELETE FROM Employees
WHERE EmployeeID = All_Employees(i);
COMMIT;
Deleted_Employees.EXTEND;
Deleted_Employees(Deleted_Employees.COUNT) := All_Employees(i);
DBMS_OUTPUT.PUT_LINE( All_Employees(i) || ' deleted' );
EXCEPTION
WHEN others THEN
ROLLBACK;
Error_Employees.EXTEND;
Error_Employees(Error_Employees.COUNT) := All_Employees(i);
DBMS_OUTPUT.PUT_LINE( All_Employees(i) || ' error - ' || SQLERRM );
END;
END LOOP;
-- Do something with the errors
END;
It is not going to be the fastest with a COMMIT at the end of each loop but it does ensure you can ROLLBACK each employee.
If you are aware of any trouble during deletion and still want to do entire operation without cursor you could use DML Error Logging:
In some situations the most obvious solution to a problem is a DML statement (INSERT ... SELECT, UPDATE, DELETE), but you may choose to avoid DML because of the way it reacts to exceptions.
By default, when a DML statement fails the whole statement is rolled back, regardless of how many rows were processed successfully before the error was detected.
In the past, the only way around this problem was to process each row individually, preferably with a bulk operation using FORALL and the SAVE EXCEPTIONS clause. In Oracle 10g Database Release 2, the DML error logging feature has been introduced to solve this problem. Adding the appropriate LOG ERRORS clause on to most INSERT, UPDATE, MERGE and DELETE statements enables the operations to complete, regardless of errors.
BEGIN
DBMS_ERRLOG.create_error_log (dml_table_name => 'EmployeeName');
END;
/
Delete from EmployeeName
where EmployeeId in (select EmployeeId from temp)
LOG ERRORS INTO err$_EmployeeName ('DELETE') REJECT LIMIT UNLIMITED;

Oracle equivalent for SQL Server INSERTED and DELETED tables

I am in the process of migrating a SQL Server database to Oracle, where I have to convert SQL Server procedure which uses special tables called INSERTED and DELETED in SQL Server.
As per my understanding these tables hold copies the data of last inserted/deleted records.
(find the msdn article here: http://msdn.microsoft.com/en-us/library/ms191300.aspx)
Are there any similar tables in Oracle to achieve this..? Please advise.
UPDATE:
Thanks for your answers and comments ,I think I need to explain the situation some more. Here is the full story to understand the real scenario;
Data base contains tables and shadow tables (shadow has an additional column).
When a table is updated same changes should be recorded in relevant shadow table with some additional data.
For this purpose they are having triggers for each table (these triggers copy data to relevant shadow table).
The above mentioned procedure generates these triggers dynamically for each and every table.
Now the real problem is I don't have the knowledge about the columns as triggers are dynamically generated for each table.
Basically I can’t get value like: NEW.col_1 or: OLD.col_1 as APC mentioned. Can I.?
Or else I have to write all those triggers manually using prefixes: NEW and: OLD rather than trying to generate them dynamically.
I am using Oracle 11g
Oracle triggers use pseudo-records rather than special tables. That is, instead of tables we can access the values of individual columns.
We distinguish pseudo-records in the affected table from records in (other) tables by using the prefixes :NEW and :OLD . Oracle allows us to declare our own names for these, but there is really no good reason for abandoning the standard.
Which column values can we access?
Action :OLD :NEW
------ ---- ----
INSERTING n/a Inserted value
UPDATING Superseded value Amended value
DELETING Deleted value n/a
You will see that :OLD is the same as the MSSQL table DELETED and :NEW is the same as table INSERTED
So, to trigger a business rule check when a certain column is updated:
create or replace trigger t23_bus_check_trg
before update on t23
for each row
begin
if :NEW.col_1 != :OLD.col_1 then
check_this(:NEW.col_1 , :OLD.col_1);
end if;
end t23_bus_check_trg;
There's a whole chapter on records in the PL/SQL Reference. Find out more.
There are many differences between Sql Server triggers and Oracle triggers. In Oracle, you can declare statement level or row level triggers. Sql Server only has statement level. In Oracle, you can declare before triggers or after triggers. Sql Server only has after triggers.
If you're going to be working with Oracle, although later versions have the compound trigger, get used to working with row level triggers. There you have the pseudo row designation of :old and :new, kinda like Deleted and Inserted except it's just the one row of data. It's like being in a cursor loop, something you can do in Sql Server, but cursor perform so poorly in Sql Server, developers go to great lengths to avoid them. They are commonly used in Oracle.
The general rule of thumb is this: if you need to examine the data and possibly alter it before it goes to the table, use a "before" trigger. If you want to perform an audit or logging procedure, use an "after" trigger.
The page I linked to above gives a lot of technical details, but it is absolutely atrocious at giving usable examples. For that, just google "oracle trigger tutorial" and you should get lots of handy, easy-to-learn-from examples.
Thanks for the answers and comments. here is the complete solution to my problem.If some one meet the exact problem this will help.
create or replace PROCEDURE CreateTrackingTriggers
(
-- take the target table and shadow user as agruments
v_TableName IN NVARCHAR2 DEFAULT NULL,
v_ShadowUser IN NVARCHAR2 DEFAULT 'SHADOW_USER'
)
AUTHID CURRENT_USER -- grant permission to create triggers
AS
v_TriggerName NVARCHAR2(500);
v_ColList NVARCHAR2(2000);
v_ColList_shadow NVARCHAR2(2000);
v_SQLCommand VARCHAR2(4000);
v_ColName NVARCHAR2(500);
v_ColSize NUMBER(10,0);
v_Prefix NVARCHAR2(500);
v_count NUMBER(1,0);
BEGIN
DECLARE
-- define a cursor to get the columns of the target table. order by COLUMN_ID is important
CURSOR Cols
IS SELECT COLUMN_NAME , CHAR_COL_DECL_LENGTH FROM USER_TAB_COLS
WHERE TABLE_NAME = upper(v_TableName) order by COLUMN_ID;
-- define a cursor to get the columns of the target shadow table order by COLUMN_ID is important
CURSOR Shadow_Cols
IS SELECT COLUMN_NAME , CHAR_COL_DECL_LENGTH FROM ALL_TAB_COLS
WHERE TABLE_NAME = upper(v_TableName) and upper(owner)=upper(v_ShadowUser) order by COLUMN_ID;
BEGIN
-- generate the trigger name for target table
v_TriggerName := 'TRG_' || upper(v_TableName) || '_Track' ;
-- check v_count , determine whether shdow table exist if not handle it
select count(*) into v_count from all_tables where table_name = upper(v_TableName) and owner = upper(v_ShadowUser);
-- iterate the cursor. generating column names prefixing ':new.'
OPEN Cols;
FETCH Cols INTO v_ColName,v_ColSize;
WHILE Cols%FOUND
LOOP
BEGIN
IF v_ColList IS NULL THEN
v_ColList := ':new.'||v_ColName ;
ELSE
v_ColList := v_ColList || ',' || ':new.'||v_ColName;
END IF;
FETCH Cols INTO v_ColName,v_ColSize;
END;
END LOOP;
CLOSE Cols;
-- iterate the cursor. get the shadow table columns
OPEN Shadow_Cols;
FETCH Shadow_Cols INTO v_ColName,v_ColSize;
WHILE Shadow_Cols%FOUND
LOOP
BEGIN
IF v_ColList_shadow IS NULL THEN
v_ColList_shadow := v_ColName;
ELSE
v_ColList_shadow := v_ColList_shadow || ',' || v_ColName;
END IF;
FETCH Shadow_Cols INTO v_ColName,v_ColSize;
END;
END LOOP;
CLOSE Shadow_Cols;
-- create trigger command. This will generate the trigger that dupilicates target table's data into shdow table
v_SQLCommand := 'CREATE or REPLACE TRIGGER '||v_TriggerName||'
AFTER INSERT OR UPDATE OR DELETE ON '||upper(v_TableName)||'
REFERENCING OLD AS old NEW AS new
FOR EACH ROW
DECLARE
ErrorCode NUMBER(19,0);
BEGIN
-- v_ColList_shadow : shdow table column list
-- v_ColList : target table column list with :new prefixed
INSERT INTO '|| v_ShadowUser ||'.'||upper(v_TableName)||'('||v_ColList_shadow||') values ('||v_ColList||');
EXCEPTION
WHEN OTHERS THEN ErrorCode := SQLCODE;
END;';
EXECUTE IMMEDIATE v_SQLCommand;
END;
END;

Dynamic Cursors

I use a cursor for the statement:
SELECT NAME FROM STUDENT WHERE ROLL = 1;
I used:
CURSOR C IS SELECT NAME FROM STUDENT WHERE ROLL = roll;
--roll is a variable I receive via a procedure, and the procedure works fine for the received parameter.
Upon executing this, I am able to retrieve all records with roll = 1.
Now, I need to retrieve the records of a group (possibly via a cursor), just like:
SELECT NAME FROM STUDENT WHERE ROLL IN (2, 4, 6);
But the values in the IN clause are known only at run time. How should I do this? That is, is there any way I could assign parameters to the WHERE clause of the cursor?
I tried using an array in the declaration of the cursor, but an error pops up telling something like: standard types cannot be used.
I used:
CURSOR C IS SELECT NAME FROM STUDENT WHERE ROLL IN (rolls);
--rolls is an array initialized with the required roll numbers.
First, I assume that the parameter to your procedure doesn't actually match the name of a column in the STUDENT table. If you actually coded the statement you posted, roll would be resolved as the name of the column, not the parameter or local variable so this statement would return every row in the STUDENT table where the ROLL column was NOT NULL.
CURSOR C
IS SELECT NAME
FROM STUDENT
WHERE ROLL = roll;
Second, while it is possible to use dynamic SQL as #Gaurav Soni suggests, doing so generates a bunch of non-sharable SQL statements. That's going to flood the shared pool, probably aging other statements out of cache, and use a lot of CPU hard-parsing the statement every time. Oracle is built on the premise that you are going to parse a SQL statement once, generally using bind variables, and then execute the statement many times with different values for the bind variables. Oracle can go through the process of parsing the query, generating the query plan, placing the query in the shared pool, etc. only once and then reuse all that when you execute the query again. If you generate a bunch of SQL statements that will never be used again because you're using dynamic SQL without bind variables, Oracle is going to end up spending a lot of time caching SQL statements that will never be executed again, pushing useful cached statements that will be used again out of the shared pool meaning that you're going to have to re-parse those queries the next time they're encountered.
Additionally, you've opened yourself up to SQL injection attacks. An attacker can exploit the procedure to read any data from any table or execute any function that the owner of the stored procedure has access to. That is going to be a major security hole even if your application isn't particularly security conscious.
You would be better off using a collection. That prevents SQL injection attacks and it generates a single sharable SQL statement so you don't have to do constant hard parses.
SQL> create type empno_tbl is table of number;
2 /
Type created.
SQL> create or replace procedure get_emps( p_empno_arr in empno_tbl )
2 is
3 begin
4 for e in (select *
5 from emp
6 where empno in (select column_value
7 from table( p_empno_arr )))
8 loop
9 dbms_output.put_line( e.ename );
10 end loop;
11 end;
12 /
Procedure created.
SQL> set serveroutput on;
SQL> begin
2 get_emps( empno_tbl( 7369,7499,7934 ));
3 end;
4 /
SMITH
ALLEN
MILLER
PL/SQL procedure successfully completed.
create or replace procedure dynamic_cur(p_empno VARCHAR2) IS
cur sys_refcursor;
v_ename emp.ename%type;
begin
open cur for 'select ename from emp where empno in (' || p_empno || ')';
loop
fetch cur into v_ename;
exit when cur%notfound;
dbms_output.put_line(v_ename);
end loop;
close cur;
end dynamic_cur;
Procedure created
Run the procedure dynamic_cur
declare
v_empno varchar2(200) := '7499,7521,7566';
begin
dynamic_cur(v_empno);
end;
Output
ALLEN
WARD
JONES
Note:As mentioned by XQbert,dynamic cursor leads to SQL injection ,but if you're not working on any critical requirement ,where security is not involved then you can use this .
Maybe you can pass rolls as a set of quoted comma separated values.
e.g. '1', '2' etc
If this value is passes into the procedure in a varchar input variable, the it can be used to get multiple rows as per the table match.
Hence the cursor
SELECT NAME FROM STUDENT WHERE ROLL IN (rolls);
will be evaluated as
SELECT NAME FROM STUDENT WHERE ROLL IN ('1','2');
Hope it helps

How can I insert into one table and update another table with the same sql statement?

The requirement to do it in one statement is because of how the program handles sql statements. The sql statement is stored in a column of another table, and executed through an open on a recordset. The open responds with an error of invalid character if a semi-colon is in the statement.
The scenario: Under certain conditions, I want to update a particular field in one record in database A, and record the fact of that change in a log table by an insert.
Here's an example using two statements:
update data_table a set field1='new value' where identifier=10;
insert into log_table (action_taken)
values('record ' || a.identifier || ' had field1 changed to ' || a.field1);
Is there any way to do this?
Put them both in a stored procedure and execute call the stored procedure.
I think you are looking for TRIGGERS. Without knowing what database you are using I can only guess.
Here is information about triggers for MySQL.
A trigger is tied to a table to start on a specific event, such as INSERT, UPDATE or DELETE. The trigger can then run one or more SQL statements.
And here is how you create a trigger
Make a batch call to the SGDB you can do what you have done, if you are using java or .net both of the their SQL API support sql batch commands.
update data_table a set field1='new value' where identifier=10;
GO
insert into log_table (action_taken)
values('record ' || a.identifier || ' had field1 changed to ' || a.field1);
GO
But if you are going to do this two operations at the same time all the time you should accept krefftc answer because it's the best way
If your database happens to be Oracle you may be able to use an anonymous PL/SQL block if for some reason you can't/don't want to create a stored procedure:
BEGIN
update data_table a set field1='new value' where identifier=10;
insert into log_table (action_taken)
values('record ' || a.identifier || ' had field1 changed to ' || a.field1);
END;