I'm trying to implement an undo and logging feature for my project.
When a user deletes a row from a table with the DELETE_ROW procedure i select all values from that row and insert it into my row_history table by serializing row values as xml with LOG_DELETED_ROW procedure, then i delete row from its original table.
Serializing with built-in functions of Oracle was easy but i couldn't find a way to deserialize the rowdata and insert it back to own table.
Is there any way to store that deleted row into another table and restore it when needed?
Delete Procedure:
create or replace procedure DELETE_ROW(tableName varchar2, userId varchar2, columnName varchar2, columnValue number) is
begin
log_deleted_row(tableName, userId, columnName, columnValue);
execute immediate 'delete from ' || tableName || ' where ' || columnName || ' = ' || columnValue;
end DELETE_ROW;
Logging Procedure:
create or replace procedure LOG_DELETED_ROW(tableName varchar2, userId varchar2, columnName varchar2, columnValue number) is
tableId number;
begin
SELECT ID into tableId FROM TABLES WHERE NAME = tableName;
execute immediate
'INSERT INTO ROW_HISTORY(TABLE_ID,ROW_ID,ROW_DATA)
SELECT
'|| tableId ||',
'|| columnValue ||',
to_clob(
DBMS_XMLGEN.getxmltype(
''SELECT * FROM ' || tableName || ' where ' || columnName || ' = ' || columnValue || '''
)
)FROM DUAL';
end LOG_DELETED_ROW;
Row History Table:
create table ROW_HISTORY
(
ID NUMBER not null,
TABLE_ID NUMBER not null,
ROW_ID NUMBER not null,
ROW_DATA CLOB not null
)
DBMS_XMLSAVE seems to be the thing you need.Here is a procedure which should do what you need to do.
CREATE OR REPLACE PROCEDURE insert_xml_data(p_table IN VARCHAR2, xml_data IN CLOB) IS
t_context DBMS_XMLSAVE.CTXTYPE;
t_rows NUMBER;
BEGIN
t_context := DBMS_XMLSAVE.NEWCONTEXT(p_table);
t_rows := DBMS_XMLSAVE.INSERTXML(t_context,xml_data);
DBMS_XMLSAVE.CLOSECONTEXT(t_context);
END;
/
I believe you could use DBMS_SQL package here - it will allow you to reconstruct insert statement knowing table name and columns.
Another, more complicated, way would be to insantiate LCR$_ROW_RECORD object and then run its EXECUTE member - it will perform actual insert.
Related
I need to verify converted data, distinct values and records counts. I would like to write statements so that I can enter a table name, then retrieve it's columns and use them in a query to get its distinct values (the actual values, not just a count of how many distinct) and their count.
I think I need to a CURSOR or CURSOR FOR LOOP and create something like this:
declare
cursor field_name
is
select COLUMN_NAME
from user_tab_cols
where table_name='TABLE1'
c_field_name field_name%ROWTYPE;
BEGIN
OPEN field_name
loop
fetch field_name INTO c_field_name;
exit when field_name%NOTFOUND;
end loop;
CLOSE field_name;
end;
Then run a query using that above in something like
select field_name, count(*)
from table1
group by field_name
Do I need to create 2 loop statements? I've not yet created one and can't quite get the context to get my results so far.
BEGIN
FOR myrow in (select field_name, count(*) as "count" from table1 group by field_name)
loop
dbms_output.put_line(myrow.field_name);
dbms_output.put_line(myrow.count);
end loop;
end;
Considering you will be giving the table name as parameter below code will print all the values of all the columns one by one along with the count of the values
create or replace PROCEDURE PR_PREP(
P_TABLE_NAME IN VARCHAR2)
IS
CURSOR CUR_COLUMNS (PA_TABLE_NAME VARCHAR2)
IS
SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS WHERE TABLE_NAME = PA_TABLE_NAME;
COL_NAMES CUR_COLUMNS%ROWTYPE;
TYPE TYP_RECORD
IS
RECORD
(
FIELD_NAME VARCHAR2(255),
CNT INT);
TYPE TYP_OP_TABLE
IS
TABLE OF TYP_RECORD;
OP_TABLE TYP_OP_TABLE;
I INT;
V_SQL VARCHAR2(2000);
BEGIN
FOR COL_NAMES IN CUR_COLUMNS(P_TABLE_NAME)
LOOP
V_SQL := 'SELECT ' || COL_NAMES.COLUMN_NAME || ' AS FIELD_NAME ,
COUNT(*) AS CNT FROM ' ||
P_TABLE_NAME || ' GROUP BY ' || COL_NAMES.COLUMN_NAME ;
-- DBMS_OUTPUT.PUT_LINE (V_SQL);
EXECUTE IMMEDIATE V_SQL BULK COLLECT INTO OP_TABLE;
dbms_output.put_line('columna name = ' ||COL_NAMES.COLUMN_NAME);
FOR I IN OP_TABLE.FIRST .. OP_TABLE.LAST
LOOP
DBMS_OUTPUT.PUT_LINE('FIELD VALUE '||OP_TABLE(I).FIELD_NAME || ' COUNT = ' || OP_TABLE(I).CNT);
END LOOP;
DBMS_OUTPUT.PUT_LINE('ONE FILED ENDED , NEXT STARTED');
END LOOP;
END;
I am using the following Procedure in pl sql
CREATE OR REPLACE "MY_PROCEDURE" (v_id number, v_name varchar2) AS
BEGIN
EXECUTE IMMEDIATE ' CREATE TABLE T_TEMPO ( t_id number , t_name varchar2(250) , t_value number )';
EXECUTE IMMEDIATE 'INSERT INTO T_TEMPO (t_id , t_name, t_value)
SELECT id , name , value
from TABLE_2
where TABLE_2.id = || v_id ||
AND TABLE_2.name = || v_name || ';
But this doesn't work I have a missing expression error, I wonder it can't evaluate the value of the variables v_id and v_name inside the execute immediate.
Can anyone help please?
You'll either need to quote the values (using quotation marks where necessary):
create or replace procedure my_procedure
( v_id number
, v_name varchar2 )
as
begin
execute immediate 'CREATE TABLE T_TEMPO (t_id number, t_name varchar2(250), t_value number )';
execute immediate 'INSERT INTO T_TEMPO (t_id, t_name, t_value)
SELECT id, name, value
FROM table_2
WHERE table_2.id = ' || v_id ||
' AND table_2.name = ''' || v_name || '''';
end my_procedure;
or (generally better) pass them as bind variables:
create or replace procedure my_procedure
( v_id number
, v_name varchar2 )
as
begin
execute immediate 'CREATE TABLE T_TEMPO (t_id number, t_name varchar2(250), t_value number )';
execute immediate 'INSERT INTO T_TEMPO (t_id, t_name, t_value)
SELECT id, name, value
FROM table_2
WHERE table_2.id = :id
AND table_2.name = :name' using v_id, v_name;
end my_procedure;
btw I would use the v prefix for variables and p for parameters.
Please use variables outside quotation:
EXECUTE IMMEDIATE ' CREATE TABLE T_TEMPO ( t_id number , t_name varchar2(250) , t_value number )';
EXECUTE IMMEDIATE 'INSERT INTO T_TEMPO (t_id , t_name, t_value)
SELECT id , name , value
from TABLE_2
where TABLE_2.id = ' || v_id ||
' AND TABLE_2.name = '''|| v_name || '''';
I have a table called PKCHANGES that has a few columns, one of which is the primary_key column. What I want is to create a trigger on other tables, and upon an insert I grab some values and post them to the PKCHANGES table. All is fine except for when I try and post the primary key values. I want that in the column primary_key I input the primary key values comma delimited. So if TableX has 3 primary keys, in PKCHANGES (primary_key column) I post value1,value2,value3.
So far I am only managing to get the below as a result and not the actual values
":new.pkCol1:new.pkCol2:new.pkCol3"
My pl/sql block is:
DECLARE
mySql varchar2(5000);
myTable varchar2(10) := 'TableX';
BEGIN
mySql := 'CREATE OR REPLACE TRIGGER ' || 't_1' || ' AFTER INSERT ON ' || myTable || '
FOR EACH ROW
DECLARE
currentPK varchar2(200); --Contains the current primary key value in the loop
result varchar2(200); --Contains the appended string of primary key values
--Cursor that contains primaryKeys for table
CURSOR pks IS
SELECT cols.column_name FROM all_constraints cons, all_cons_columns cols
WHERE cons.constraint_type = ''P''
AND cons.constraint_name = cols.constraint_name
AND cons.table_name = ' || '''' || myTable || '''' || ';
BEGIN
--Loop through primary keys, get the value from the trigger, and append the string.
for current_pk IN pks LOOP
BEGIN
currentPK := '':new.'' || current_pk.column_name;
result:= result || currentPK;
END;
END LOOP;'
||
' --Insert the appended values into the primary_key column
INSERT INTO PKCHANGES(primary_key)' ||
'VALUES (result);'
|| ' END;';
dbms_output.put_line(mySql);
EXECUTE IMMEDIATE mySql;
END;
Any idea?
The primary key of the TableX need not be queried on each insert. It is stable and if once changed, you will change the trigger as well.
This allows you to pop up the logic out of the trigger.
In the first step concatenate the PK. I'd prefer LISTAGG as it handels elegant the delimiter. You get something like :new.COL1||','||:new.COL2||','||:new.COL3
Also make sure the table name is in correct case (I asume upper case; otherwise you need to quote the name).
In the next step generate the trigger, that will basicaly contain only the INSERT
DECLARE
mySql varchar2(5000);
myTable varchar2(10) := 'TableX';
result varchar2(200); -- Contains the concatenated string of primary key column names with delimiters,
-- e.g. ":new.COL1||','||:new.COL2||','||:new.COL3"
BEGIN
SELECT listagg(':new.'||cols.column_name,'||'',''||') within group (order by position) into result
FROM all_constraints cons, all_cons_columns cols
WHERE cons.constraint_type = 'P'
AND cons.constraint_name = cols.constraint_name
AND cons.table_name = upper(myTable);
mySql := 'CREATE OR REPLACE TRIGGER ' || 't_1' || ' AFTER INSERT ON ' || myTable || '
FOR EACH ROW
BEGIN
--Insert the appended values into the primary_key column
INSERT INTO PKCHANGES(primary_key)' ||
'VALUES ('||result||');'
|| ' END;';
dbms_output.put_line(mySql);
EXECUTE IMMEDIATE mySql;
END;
/
Test
create table TableX
(col1 number,
col2 number,
col3 number,
col4 number);
alter table TableX add (primary key (col1, col2, col3));
insert into TableX values (1,2,3,4);
select * from PKCHANGES;
PRIMARY_KEY
-----------
1,2,3
create table Employee(Id number,
Name varchar(20),
Age number,
DId number,
Salary number,
primary key(Id),
foreign key(DId) references Department on delete cascade);
declare
total number;
procedure myFunction(x in number) is
begin
insert into Employee values(17,'Jaskaran Singh',31,1,200000);
dbms_output.put_line('successfully executed');
select * from Employee;
end;
begin
myFunction(3);
end;
To return data from stored procedure, you should create a cursor and return that select like the following:
CREATE OR REPLACE PACKAGE TYPES
AS
TYPE DATA_CURSOR IS REF CURSOR;
END;
then in your code is going to be like:
CREATE OR REPLACE PROCEDURE MYPROC(RESULTSET OUT TYPES.DATA_CURSOR) AS
BEGIN
INSERT INTO EMPLOYEE VALUES(17,'Jaskaran Singh',31,1,200000);
DBMS_OUTPUT.PUT_LINE('successfully executed');
OPEN RESULTSET FOR
SELECT * FROM EMPLOYEE;
END;
The Execution part like
DECLARE THE_RESULT_SET OUT TYPES.DATA_CURSOR;
BEGIN
MYPROC(3, THE_RESULT_SET);
-- You can now get the THE_RESULT_SET and take the result from it...
END;
Important: if you want to print as I understand the other case, you can get that result (same code), and loop whatever you want and print the result from the THE_RESULT_SET
If you want to print what's in the EMPLOYEES table you have loop a cursor over the EMPLOYEES table, printing each row appropriately. Here's an example:
DECLARE
TOTAL NUMBER;
PROCEDURE MYFUNCTION(X IN NUMBER) IS
BEGIN
INSERT INTO EMPLOYEE VALUES(17,'Jaskaran Singh',31,1,200000);
DBMS_OUTPUT.PUT_LINE('successfully executed');
FOR aRow IN (SELECT * FROM EMPLOYEE) LOOP
DBMS_OUTPUT.PUT_LINE('ID=' || aRow.ID ||
' NAME=''' || aRow.NAME || '''' ||
' AGE=' || aRow.AGE ||
' DID=' || aRow.DID ||
' SALARY=' || aRow.SALARY);
END LOOP;
END;
BEGIN
MYFUNCTION(3);
END;
Share and enjoy.
I have this SQL:
DROP TABLE MISSINGTABLE;
CREATE TABLE MISSINGTABLE (
TABLE_NAME VARCHAR2 (70),
DESCRIPTION VARCHAR2 (1000)
)
CREATE OR REPLACE PROCEDURE MISSINGTABLES AS
BEGIN
INSERT INTO MISSINGTABLE
((((SELECT TABLE_NAME, 'Missing Table on PEKA_ERP_001' Description FROM ALL_TABLES WHERE OWNER = 'ASE_ERP_001')
MINUS
(SELECT TABLE_NAME, 'Missing Table on PEKA_ERP_001' Description FROM ALL_TABLES WHERE OWNER = 'PEKA_ERP_001'))
UNION
((SELECT TABLE_NAME, 'Missing Table on ASE_ERP_001' Description FROM ALL_TABLES WHERE OWNER = 'PEKA_ERP_001')
MINUS
(SELECT TABLE_NAME, 'Missing Table on ASE_ERP_001' Description FROM ALL_TABLES WHERE OWNER = 'ASE_ERP_001'))));
END;
So, how u can see, I'm creating a Table and then a Procedure, which fills the Table.
Now I want 2 Variables for these Arguments: 'PEKA_ERP_001' and 'ASE_ERP_001' (so I don't always need to write it manually, because this values changes a lot)
I tried this (included only the first part of above Statement):
DECLARE
S1 VARCHAR2(100) := 'ASE_ERP_001';
S2 VARCHAR2(100) := 'PEKA_ERP_001';
TableMissing VARCHAR(100) := 'Missing Table on ';
Apostrophe VARCHAR(10) := '''';
BEGIN
EXECUTE IMMEDIATE ('CREATE OR REPLACE PROCEDURE MISSINGTABLES AS BEGIN INSERT INTO MISSINGTABLE (SELECT TABLE_NAME, ' || Apostrophe || TableMissing || S2 || Apostrophe || ' Description FROM ALL_TAB_COLUMNS WHERE OWNER = ' || Apostrophe || S1 || Apostrophe || ')' || ' END;');
END;
It creates The Procedure, but the Procedure contains the "CREATE OR REPLACE PROCEDURE" itself and its showing me an error... (I cannot execute the Procedure)
Can anyone help me? How can I write the first SQL Statement at the Head which works, only with 2 Variables more, ASE_ERP_001 and PEKA_ERP_001 ?
EDIT:
Statement:
DECLARE
S1 VARCHAR2(100) := 'ASE_ERP_001';
S2 VARCHAR2(100) := 'PEKA_ERP_001';
TabelleFehlt VARCHAR(100) := 'Diese Tabelle fehlt ';
Hochkomma VARCHAR(10) := '''';
BEGIN
EXECUTE IMMEDIATE ('CREATE OR REPLACE PROCEDURE MISSINGTABLES AS BEGIN INSERT INTO MISSINGTABLE (SELECT TABLE_NAME, ' || Hochkomma || TabelleFehlt || S2 || Hochkomma || ' Beschreibung FROM ALL_TAB_COLUMNS WHERE OWNER = ' || Hochkomma || S1 || Hochkomma || ') END;');
END;
The Statement Above Creates a Procedure.
But it also shows me this:
ORA-06512: in Row 7
24344. 00000 - "success with compilation error"
*Cause: A sql/plsql compilation error occurred.
*Action: Return OCI_SUCCESS_WITH_INFO along with the error code
And The PROCEDURE Itselfs Contains this:
create or replace
PROCEDURE MISSINGTABLES AS BEGIN INSERT INTO MISSINGTABLE (SELECT TABLE_NAME, 'Diese Tabelle fehlt PEKA_ERP_001' Beschreibung FROM ALL_TAB_COLUMNS WHERE OWNER = 'ASE_ERP_001') END;
But it should not Contain "Create or Replace Procedure MISSINGTABLES" etc. only the INSERT STatement, I cannot execute the Procedure anyway..
even better would be to use the script from bpgergo, if it would go.
I hope I did not mix the arguments up, you should check them again
CREATE OR REPLACE PROCEDURE MISSINGTABLES (p_1 in varchar2, p_2 in varchar2)
AS
BEGIN
INSERT INTO MISSINGTABLE
((((SELECT TABLE_NAME, 'Missing Table on '||p_1 Description FROM ALL_TABLES WHERE OWNER = p_2)
MINUS
(SELECT TABLE_NAME, 'Missing Table on '||p_1 Description FROM ALL_TABLES WHERE OWNER = p_1))
UNION
((SELECT TABLE_NAME, 'Missing Table on '||p_2 Description FROM ALL_TABLES WHERE OWNER = p_1)
MINUS
(SELECT TABLE_NAME, 'Missing Table on '||p_2 Description FROM ALL_TABLES WHERE OWNER = p_2))));
END;
EDIT
you would call this like:
begin
MISSINGTABLES ('PEKA_ERP_001', 'ASE_ERP_001');
end;
The SQL that you are trying to execute immediate will be evaluated as:
CREATE OR REPLACE PROCEDURE MISSINGTABLES AS
BEGIN
INSERT INTO MISSINGTABLE
(SELECT TABLE_NAME, COLUMN_NAME, 'Missing Table on PEKA_ERP_001' Beschreibung
FROM ALL_TAB_COLUMNS WHERE OWNER = 'ASE_ERP_001')
END;
This probably isn't the logic that you actually want, but the immediate problem is that you are trying to populate a non-existant third column called Beschreibung instead of populating the second column, DESCRIPTION .
Might I suggest an improvement to your SELECT?
Here's a possible alternative:
SELECT
TABLE_NAME,
'Missing Table on'
|| CASE MAX(OWNER) WHEN 'PEKA_ERP_001' THEN 'ASE_ERP_001' ELSE 'PEKA_ERP_001' END
AS Description
FROM ALL_TABLES
WHERE OWNER IN ('PEKA_ERP_001', 'ASE_ERP_001')
GROUP BY TABLE_NAME
HAVING COUNT(*) = 1
This query returns only rows where a TABLE_NAME has just one OWNER. The owner that is missing the table is then shown to be as the other one of the two being tested.
Using parameters, the entire CREATE PROCEDURE statement might look like this:
CREATE OR REPLACE PROCEDURE MISSINGTABLES
(
owner1 IN varchar2,
owner2 IN varchar2
)
AS
BEGIN
INSERT INTO MISSINGTABLE
(
SELECT
TABLE_NAME,
'Missing Table on'
|| CASE MAX(OWNER) WHEN owner1 THEN owner2 ELSE owner1 END
AS Description
FROM ALL_TABLES
WHERE OWNER IN (owner1, owner2)
GROUP BY TABLE_NAME
HAVING COUNT(*) = 1
);
END;