Dynamic query with :new value - sql

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

Related

Adding a new column at certain place in Postgres [duplicate]

How to add a new column in a table after the 2nd or 3rd column in the table using postgres?
My code looks as follows
ALTER TABLE n_domains ADD COLUMN contract_nr int after owner_id
No, there's no direct way to do that. And there's a reason for it - every query should list all the fields it needs in whatever order (and format etc) it needs them, thus making the order of the columns in one table insignificant.
If you really need to do that I can think of one workaround:
dump and save the description of the table in question (using pg_dump --schema-only --table=<schema.table> ...)
add the column you want where you want it in the saved definition
rename the table in the saved definition so not to clash with the name of the old table when you attempt to create it
create the new table using this definition
populate the new table with the data from the old table using 'INSERT INTO <new_table> SELECT field1, field2, <default_for_new_field>, field3,... FROM <old_table>';
rename the old table
rename the new table to the original name
eventually drop the old, renamed table after you make sure everything's alright
The order of columns is not irrelevant, putting fixed width columns at the front of the table can optimize the storage layout of your data, it can also make working with your data easier outside of your application code.
PostgreSQL does not support altering the column ordering (see Alter column position on the PostgreSQL wiki); if the table is relatively isolated, your best bet is to recreate the table:
CREATE TABLE foobar_new ( ... );
INSERT INTO foobar_new SELECT ... FROM foobar;
DROP TABLE foobar CASCADE;
ALTER TABLE foobar_new RENAME TO foobar;
If you have a lot of views or constraints defined against the table, you can re-add all the columns after the new column and drop the original columns (see the PostgreSQL wiki for an example).
The real problem here is that it's not done yet. Currently PostgreSQL's logical ordering is the same as the physical ordering. That's problematic because you can't get a different logical ordering, but it's even worse because the table isn't physically packed automatically, so by moving columns you can get different performance characteristics.
Arguing that it's that way by intent in design is pointless. It's somewhat likely to change at some point when an acceptable patch is submitted.
All of that said, is it a good idea to rely on the ordinal positioning of columns, logical or physical? Hell no. In production code you should never be using an implicit ordering or *. Why make the code more brittle than it needs to be? Correctness should always be a higher priority than saving a few keystrokes.
As a work around, you can in fact modify the column ordering by recreating the table, or through the "add and reorder" game
See also,
Column tetris reordering in order to make things more space-efficient
The column order is relevant to me, so I created this function. See if it helps. It works with indexes, primary key, and triggers. Missing Views and Foreign Key and other features are missing.
Example:
SELECT xaddcolumn('table', 'col3 int NOT NULL DEFAULT 0', 'col2');
Source code:
CREATE OR REPLACE FUNCTION xaddcolumn(ptable text, pcol text, pafter text) RETURNS void AS $BODY$
DECLARE
rcol RECORD;
rkey RECORD;
ridx RECORD;
rtgr RECORD;
vsql text;
vkey text;
vidx text;
cidx text;
vtgr text;
ctgr text;
etgr text;
vseq text;
vtype text;
vcols text;
BEGIN
EXECUTE 'CREATE TABLE zzz_' || ptable || ' AS SELECT * FROM ' || ptable;
--colunas
vseq = '';
vcols = '';
vsql = 'CREATE TABLE ' || ptable || '(';
FOR rcol IN SELECT column_name as col, udt_name as coltype, column_default as coldef,
is_nullable as is_null, character_maximum_length as len,
numeric_precision as num_prec, numeric_scale as num_scale
FROM information_schema.columns
WHERE table_name = ptable
ORDER BY ordinal_position
LOOP
vtype = rcol.coltype;
IF (substr(rcol.coldef,1,7) = 'nextval') THEN
vtype = 'serial';
vseq = vseq || 'SELECT setval(''' || ptable || '_' || rcol.col || '_seq'''
|| ', max(' || rcol.col || ')) FROM ' || ptable || ';';
ELSIF (vtype = 'bpchar') THEN
vtype = 'char';
END IF;
vsql = vsql || E'\n' || rcol.col || ' ' || vtype;
IF (vtype in ('varchar', 'char')) THEN
vsql = vsql || '(' || rcol.len || ')';
ELSIF (vtype = 'numeric') THEN
vsql = vsql || '(' || rcol.num_prec || ',' || rcol.num_scale || ')';
END IF;
IF (rcol.is_null = 'NO') THEN
vsql = vsql || ' NOT NULL';
END IF;
IF (rcol.coldef <> '' AND vtype <> 'serial') THEN
vsql = vsql || ' DEFAULT ' || rcol.coldef;
END IF;
vsql = vsql || E',';
vcols = vcols || rcol.col || ',';
--
IF (rcol.col = pafter) THEN
vsql = vsql || E'\n' || pcol || ',';
END IF;
END LOOP;
vcols = substr(vcols,1,length(vcols)-1);
--keys
vkey = '';
FOR rkey IN SELECT constraint_name as name, column_name as col
FROM information_schema.key_column_usage
WHERE table_name = ptable
LOOP
IF (vkey = '') THEN
vkey = E'\nCONSTRAINT ' || rkey.name || ' PRIMARY KEY (';
END IF;
vkey = vkey || rkey.col || ',';
END LOOP;
IF (vkey <> '') THEN
vsql = vsql || substr(vkey,1,length(vkey)-1) || ') ';
END IF;
vsql = substr(vsql,1,length(vsql)-1) || ') WITHOUT OIDS';
--index
vidx = '';
cidx = '';
FOR ridx IN SELECT s.indexrelname as nome, a.attname as col
FROM pg_index i LEFT JOIN pg_class c ON c.oid = i.indrelid
LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
LEFT JOIN pg_stat_user_indexes s USING (indexrelid)
WHERE c.relname = ptable AND i.indisunique != 't' AND i.indisprimary != 't'
ORDER BY s.indexrelname
LOOP
IF (ridx.nome <> cidx) THEN
IF (vidx <> '') THEN
vidx = substr(vidx,1,length(vidx)-1) || ');';
END IF;
cidx = ridx.nome;
vidx = vidx || E'\nCREATE INDEX ' || cidx || ' ON ' || ptable || ' (';
END IF;
vidx = vidx || ridx.col || ',';
END LOOP;
IF (vidx <> '') THEN
vidx = substr(vidx,1,length(vidx)-1) || ')';
END IF;
--trigger
vtgr = '';
ctgr = '';
etgr = '';
FOR rtgr IN SELECT trigger_name as nome, event_manipulation as eve,
action_statement as act, condition_timing as cond
FROM information_schema.triggers
WHERE event_object_table = ptable
LOOP
IF (rtgr.nome <> ctgr) THEN
IF (vtgr <> '') THEN
vtgr = replace(vtgr, '_#eve_', substr(etgr,1,length(etgr)-3));
END IF;
etgr = '';
ctgr = rtgr.nome;
vtgr = vtgr || 'CREATE TRIGGER ' || ctgr || ' ' || rtgr.cond || ' _#eve_ '
|| 'ON ' || ptable || ' FOR EACH ROW ' || rtgr.act || ';';
END IF;
etgr = etgr || rtgr.eve || ' OR ';
END LOOP;
IF (vtgr <> '') THEN
vtgr = replace(vtgr, '_#eve_', substr(etgr,1,length(etgr)-3));
END IF;
--exclui velha e cria nova
EXECUTE 'DROP TABLE ' || ptable;
IF (EXISTS (SELECT sequence_name FROM information_schema.sequences
WHERE sequence_name = ptable||'_id_seq'))
THEN
EXECUTE 'DROP SEQUENCE '||ptable||'_id_seq';
END IF;
EXECUTE vsql;
--dados na nova
EXECUTE 'INSERT INTO ' || ptable || '(' || vcols || ')' ||
E'\nSELECT ' || vcols || ' FROM zzz_' || ptable;
EXECUTE vseq;
EXECUTE vidx;
EXECUTE vtgr;
EXECUTE 'DROP TABLE zzz_' || ptable;
END;
$BODY$ LANGUAGE plpgsql VOLATILE COST 100;
#Jeremy Gustie's solution above almost works, but will do the wrong thing if the ordinals are off (or fail altogether if the re-ordered ordinals make incompatible types match). Give it a try:
CREATE TABLE test1 (one varchar, two varchar, three varchar);
CREATE TABLE test2 (three varchar, two varchar, one varchar);
INSERT INTO test1 (one, two, three) VALUES ('one', 'two', 'three');
INSERT INTO test2 SELECT * FROM test1;
SELECT * FROM test2;
The results show the problem:
testdb=> select * from test2;
three | two | one
-------+-----+-------
one | two | three
(1 row)
You can remedy this by specifying the column names in the insert:
INSERT INTO test2 (one, two, three) SELECT * FROM test1;
That gives you what you really want:
testdb=> select * from test2;
three | two | one
-------+-----+-----
three | two | one
(1 row)
The problem comes when you have legacy that doesn't do this, as I indicated above in my comment on peufeu's reply.
Update: It occurred to me that you can do the same thing with the column names in the INSERT clause by specifying the column names in the SELECT clause. You just have to reorder them to match the ordinals in the target table:
INSERT INTO test2 SELECT three, two, one FROM test1;
And you can of course do both to be very explicit:
INSERT INTO test2 (one, two, three) SELECT one, two, three FROM test1;
That gives you the same results as above, with the column values properly matched.
The order of the columns is totally irrelevant in relational databases
Yes.
For instance if you use Python, you would do :
cursor.execute( "SELECT id, name FROM users" )
for id, name in cursor:
print id, name
Or you would do :
cursor.execute( "SELECT * FROM users" )
for row in cursor:
print row['id'], row['name']
But no sane person would ever use positional results like this :
cursor.execute( "SELECT * FROM users" )
for id, name in cursor:
print id, name
Well, it's a visual goody for DBA's and can be implemented to the engine with minor performance loss. Add a column order table to pg_catalog or where it's suited best. Keep it in memory and use it before certain queries. Why overthink such a small eye candy.
# Milen A. Radev
The irrelevant need from having a set order of columns is not always defined by the query that pulls them. In the values from pg_fetch_row does not include the associated column name and therefore would require the columns to be defined by the SQL statement.
A simple select * from would require innate knowledge of the table structure, and would sometimes cause issues if the order of the columns were to change.
Using pg_fetch_assoc is a more reliable method as you can reference the column names, and therefore use a simple select * from.

Oracle get table names based on column value

I have table like this:
Table-1
Table-2
Table-3
Table-4
Table-5
each table is having many columns and one of the column name is employee_id.
Now, I want to write a query which will
1) return all the tables which is having this columns and
2) results should show the tables if the column is having values or empty values by passing employee_id.
e.g. show table name, column name from Table-1, Table-2,Table-3,... where employee_id='1234'.
If one of the table doesn't have this column, then it is not required to show.
I have verified with link, but it shows only table name and column name and not by passing some column values to it.
Also verified this, but here verifies from entire schema which I dont want to do it.
UPDATE:
Found a solution, but by using xmlsequence which is deprecated,
1)how do I make this code as xmltable?
2) If there are no values in the table, then output should have empty/null. or default as "YES" value
WITH char_cols AS
(SELECT /*+materialize */ table_name, column_name
FROM cols
WHERE data_type IN ('CHAR', 'VARCHAR2') and table_name in ('Table-1','Table-2','Table-3','Table-4','Table-5'))
SELECT DISTINCT SUBSTR (:val, 1, 11) "Employee_ID",
SUBSTR (table_name, 1, 14) "Table",
SUBSTR (column_name, 1, 14) "Column"
FROM char_cols,
TABLE (xmlsequence (dbms_xmlgen.getxmltype ('select "'
|| column_name
|| '" from "'
|| table_name
|| '" where upper("'
|| column_name
|| '") like upper(''%'
|| :val
|| '%'')' ).extract ('ROWSET/ROW/*') ) ) t ORDER BY "Table"
/
This query can be done in one step using the (non-deprecated) XMLTABLE.
Sample Schema
--Table-1 and Table-2 match the criteria.
--Table-3 has the right column but not the right value.
--Table-4 does not have the right column.
create table "Table-1" as select '1234' employee_id from dual;
create table "Table-2" as select '1234' employee_id from dual;
create table "Table-3" as select '4321' employee_id from dual;
create table "Table-4" as select 1 id from dual;
Query
--All tables with the column EMPLOYEE_ID, and the number of rows where EMPLOYEE_ID = '1234'.
select table_name, total
from
(
--Get XML results of dynamic query on relevant tables and columns.
select
dbms_xmlgen.getXMLType(
(
--Create a SELECT statement on each table, UNION ALL'ed together.
select listagg(
'select '''||table_name||''' table_name, count(*) total
from "'||table_name||'" where employee_id = ''1234'''
,' union all'||chr(10)) within group (order by table_name) v_sql
from user_tab_columns
where column_name = 'EMPLOYEE_ID'
)
) xml
from dual
) x
cross join
--Convert the XML data to relational.
xmltable('/ROWSET/ROW'
passing x.xml
columns
table_name varchar2(128) path 'TABLE_NAME',
total number path 'TOTAL'
);
Results
TABLE_NAME TOTAL
---------- -----
Table-1 1
Table-2 1
Table-3 0
Just try to use code below.
Pay your attention that may be nessecery clarify scheme name in loop.
This code works for my local db.
set serveroutput on;
DECLARE
ex_query VARCHAR(300);
num NUMBER;
emp_id number;
BEGIN
emp_id := <put your value>;
FOR rec IN
(SELECT table_name
FROM all_tab_columns
WHERE column_name LIKE upper('employee_id')
)
LOOP
num :=0;
ex_query := 'select count(*) from ' || rec.table_name || ' where employee_id = ' || emp_id;
EXECUTE IMMEDIATE ex_query into num;
if (num>0) then
DBMS_OUTPUT.PUT_LINE(rec.table_name);
end if;
END LOOP;
END;
I tried with the xml thing, but I get an error I cannot solve. Something about a zero size result. How difficult is it to solve this instead of raising exception?! Ask Oracle.
Anyway.
What you can do is use the COLS table to know what table has the employee_id column.
1) what table from table TABLE_LIKE_THIS (I assume column with table names is C) has this column?
select *
from COLS, TABLE_LIKE_THIS t
where cols.table_name = t
and cols.column_name = 'EMPLOYEE_ID'
-- think Oracle metadata/ think upper case
2) Which one has the value you are looking for: write a little chunk of Dynamic PL/SQL with EXECUTE IMMEDIATE to count the tables matching above condition
declare
v_id varchar2(10) := 'JP1829'; -- value you are looking for
v_col varchar2(20) := 'EMPLOYEE_ID'; -- column
n_c number := 0;
begin
for x in (
select table_name
from all_tab_columns cols
, TABLE_LIKE_THIS t
where cols.table_name = t.c
and cols.column_name = v_col
) loop
EXECUTE IMMEDIATE
'select count(1) from '||x.table_name
||' where Nvl('||v_col||', ''##'') = ''' ||v_id||'''' -- adding quotes around string is a little specific
INTO n_c;
if n_c > 0 then
dbms_output.put_line(n_C|| ' in ' ||x.table_name||' has '||v_col||'='||v_id);
end if;
-- idem for null values
-- ... ||' where '||v_col||' is null '
-- or
-- ... ||' where Nvl('||v_col||', ''##'') = ''##'' '
end loop;
dbms_output.put_line('done.');
end;
/
Hope this helps

Create tables whose information stored in another table using stored procedure

I need to write a stored procedure to create table(s) whose information like table_name, column_name, data_type are stored in another table as below image...
No need to worry about Primary n Foreign Key specifications.
Is it possible to do that in ORACLE?
Thanks in Advance.
Yes. Construct the DDL string in a loop and execute it with execute immediate.
For example:
begin
for r in (
select 'create table ' || td.table_name || chr(10)||'( ' ||
listagg(rpad(td.column_name,31) || td.data_type, chr(10)||', ') within group (order by id) ||
' )' as create_table
from table_definitions td
group by td.id, td.table_name
order by td.id
)
loop
dbms_output.put_line(r.create_table || ';' || chr(10));
execute immediate r.create_table;
end loop;
end;
Demo setup:
create table table_definitions
( id integer not null
, table_name varchar2(30) not null
, column_name varchar2(30) not null, data_type varchar2(30) not null
, constraint tabdef_uk unique (table_name, column_name) );
insert all
into table_definitions values (1, 'EMP', 'EMP_ID', 'NUMBER')
into table_definitions values (1, 'EMP', 'EMP_NAME', 'VARCHAR2(30)')
into table_definitions values (1, 'EMP', 'SALARY', 'NUMBER')
into table_definitions values (1, 'EMP', 'DEPT_ID', 'NUMBER')
into table_definitions values (2, 'DEPT', 'DEPT_ID', 'NUMBER')
into table_definitions values (2, 'DEPT', 'DEPT_NAME', 'VARCHAR2(30)')
into table_definitions values (2, 'DEPT', 'LOCATION', 'VARCHAR2(30)')
select * from dual;
However, there are some problems with this whole approach.
I had to add lengths to your VARCHAR2 columns. You'd need to do the same for the others if you didn't want plain NUMBER for every numeric column. Also, there is no column ordering, so they could be generated in any order. I couldn't see the purpose of the id column - was it meant to be unique, or unique within table_name? And there is no provision for NOT NULL, defaults etc.
I would be rather nervous if I came across this in a system had to work on. What is it for?
I'm sure there will be a better approach, but meanwhile you can work on the below :
Your proc code should be something like this:
select distinct id, table_name
bulk collect into ip_id, ip_tab_name
from tab_details;
for x in ip_id.first .. ip_id.last loop
select column_name, data_type bulk collect into v_col_name,
v_data_type from tab_Details where id= ip_id(x);
v_sql := 'create table ' || ip_tab_name(x) || ' ( col1 number) ';
execute immediate v_sql;
for i in v_col_name.first .. v_col_name.last loop
v_sql1 := 'alter table ' || ip_tab_name(x) || ' add ' || v_col_name(i)
|| ' ' || v_data_type(i);
execute immediate v_sql1;
end loop; -- (for i loop)
v_sql2 := 'alter table ' || ip_tab_name(x) || ' drop column col1 ';
execute immediate v_sql2;
end loop;-- (for x loop)

Loop through a list of table columns, and apply a query to them

We have our table mytable, that contains columns: id, value, description....
Now I want to apply this sql query to each column:
select distinct [column] from mytable;
Is there a way to do this?
nb. I want to provide the list of columns to loop through, rather than looping through every column in the table.
declare
l_tab_name varchar2(32) := 'MY_TABLE';
begin
for c1 in (select t.column_name from user_tab_columns t where t.table_name = l_tab_name)
loop
execute immediate 'select distinct ' || c1.column_name || ' from ' || l_tab_name;
end loop;
end;

Inserting row values into another table's column

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.