More advanced PL/SQL query result comparison - oracle

Wider story
I'm building an automatic grader of my students' queries. I'll start and stop Docker containers for each student and already know how to build the system around it. I'll send the query directly to the database container, together with my own correct query.
Question
I'd like to have 2 things when comparing two queries:
checkbox which switches "Order of columns important, yes/no"
checkbox which switches "Order of rows important, yes/no"
This is what I have for that "core" part.
sqlplus -s system/oracle#localhost:1521/xe <<EOF
set feedback off trimspool on;
spool result.txt;
(select * from cities) MINUS (select id, name from cities);
spool off;
exit;
EOF
I need some suggestions. How would you compare two queries when the order of columns is not important? What about order of rows, do I need to force ORDER BY in every task? What is the best approach to give more automatic feedback, just printing compiler errors?
I'll appreciate every tip.
P.S. I already did my fair share of googling but without much luck for pl/sql.

You can use DBMS_SQL package to create cursor and get column names. What about row order my first assumption was to use ROWNUM pseudocolumn and after addition of it use MINUS. But the problem is that the only way to guarantee the ordering is to use ORDER BY, and I cannot generate correct ORDER BY part in outer select after columns were swapped. Place that column after SELECT with substringing is bad because it is hard to manage UNION then. You can ask to place some dummy column at the topmost SELECT which you then can replace with ROWNUM as rn and then go forth. Or execute both cursors and try to compare them row by row.
The code I get is below. Here you can try it.
declare
v_sql_t varchar(32000) := 'select 1 as a, ''q'' as b from dual union all select 2, ''a'' from dual order by 1';
v_sql_s varchar(32000) := 'select 1 as a, ''q'' as b from dual union all select 2, ''a'' from dual order by 2';
v_cursor integer;
v_col_count integer;
v_cols_t dbms_sql.desc_tab;
v_cols_s dbms_sql.desc_tab;
v_cols_select varchar2(32000);
v_select_keyword_begin integer;
v_final_select varchar2(32000);
v_col_order integer := 1;
v_row_order integer := 1;
begin
v_cursor := dbms_sql.open_cursor;
dbms_sql.parse(v_cursor, v_sql_t, dbms_sql.native);
dbms_sql.describe_columns(v_cursor, v_col_count, v_cols_t);
dbms_sql.parse(v_cursor, v_sql_s, dbms_sql.native);
dbms_sql.describe_columns(v_cursor, v_col_count, v_cols_s);
--Check column order
for i in 1..v_cols_t.count loop
if v_col_order > 0 and v_cols_t(i).col_name != v_cols_s(i).col_name then
dbms_output.put_line('Wrong column order. Should be: ' || v_cols_t(i).col_name || ', got ' || v_cols_s(i).col_name);
end if;
if i = 1 then
v_cols_select := v_cols_t(i).col_name;
else
v_cols_select := v_cols_select || ',' || v_cols_t(i).col_name;
end if;
end loop;
if v_row_order > 0 then
v_cols_select := v_cols_select || ', rownum as rn';
end if;
v_final_select := 'with t as ( select ' || v_cols_select || ' from (' || v_sql_t || ' ) )' || chr(10)
|| ', s as ( select ' || v_cols_select || ' from (' || v_sql_s || ' ) )' || chr(10)
|| ', t_s as (select * from t minus select * from s)' || chr(10)
|| ', s_t as (select * from s minus select * from t)' || chr(10)
|| 'select ''T'' as src, t_s.* from t_s
union all
select ''S'' as src, s_t.* from s_t';
dbms_output.put_line(v_final_select);
end;
/

Related

Why do I get no output from this query (searching database for string)?

I'm an Oracle/PL/SQL Developer newbie, and I'm struggling to figure out how to see the output of this query:
DECLARE
ncount NUMBER;
vwhere VARCHAR2(1000) := '';
vselect VARCHAR2(1000) := ' select count(1) from ';
vsearchstr VARCHAR2(1000) := '1301 250 Sage Valley Road NW';
vline VARCHAR2(1000) := '';
istatus INTEGER;
BEGIN
DBMS_OUTPUT.ENABLE;
FOR k IN (SELECT a.table_name, a.column_name FROM user_tab_cols a WHERE a.data_type LIKE '%VARCHAR%')
LOOP
vwhere := ' where ' || k.column_name || ' = :vsearchstr ';
EXECUTE IMMEDIATE vselect || k.table_name || vwhere
INTO ncount
USING vsearchstr;
IF (ncount > 0)
THEN
dbms_output.put_line(k.column_name || ' ' || k.table_name);
ELSE
dbms_output.put_line('no output');
END IF;
END LOOP;
dbms_output.get_line(vline, istatus);
END;
I got this script from https://community.oracle.com/tech/developers/discussion/2572717/how-to-search-a-particular-string-in-whole-schema. It's supposed to find a string (vsearchstr) in the entire database. When I run this in PL/SQL Developer 14.0.6, it spits out no errors, says it took 0.172 seconds, but I don't see any output. I'm expecting the output to show under the Output tab:
I know the string '1301 250 Sage Valley Road NW' exists in the database so it should be finding it. Even if it doesn't, the ELSE block should be outputting 'no output'.
From what I understand, dbms_output.put_line() adds the given string to a buffer, and dbms_output.get_line() prints it to the output target (whatever it's set to). I understand that dbms_output needs to be enabled (hence the line DBMS_OUTPUT.ENABLE) and dbms_output.get_line() will only run after the BEGIN/END block it's in completes (I don't know if this means it has to be put outside the BEGIN/END block, but I couldn't avoid certain errors every time I did).
I've read through various stackoverflow posts about this issue, as well as a few external site:
https://docs.oracle.com/cd/F49540_01/DOC/server.815/a68001/dbms_out.htm#1000449
https://www.tutorialspoint.com/plsql/plsql_dbms_output.htm
...but nothing seems to be working.
How can I see the output, or if there's something wrong in the query above, can you tell what it is?
Thanks.
To enable output from DBMS_OUTPUT in PL/SQL Developer see this answer.
I'm looking for an alternative keyword to user_tab_cols for all schemas in the DB
Use ALL_TAB_COLS and catch the exceptions when you do not have enough privileges to read the table (and use quoted identifiers to match the case of user/table/column names):
DECLARE
found_row PLS_INTEGER;
vsearchstr VARCHAR2(1000) := '1301 250 Sage Valley Road NW';
BEGIN
FOR k IN (SELECT owner,
table_name,
column_name
FROM all_tab_cols t
WHERE data_type LIKE '%VARCHAR%'
-- Ignore columns that are too small
AND data_length >= LENGTH(vsearchstr)
-- Ignore all oracle maintained tables
-- Not supported on earlier Oracle versions
AND NOT EXISTS (
SELECT 1
FROM all_users u
WHERE t.owner = u.username
AND u.oracle_maintained = 'Y'
)
)
LOOP
DECLARE
invalid_privileges EXCEPTION;
PRAGMA EXCEPTION_INIT(invalid_privileges, -1031);
BEGIN
EXECUTE IMMEDIATE 'SELECT 1 FROM "' || k.owner || '"."' || k.table_name || '" WHERE "' || k.column_name || '" = :1 AND ROWNUM = 1'
INTO found_row
USING vsearchstr;
dbms_output.put_line('Found: ' || k.table_name || '.' || k.column_name);
EXCEPTION
WHEN invalid_privileges THEN
NULL;
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('Not found: ' || k.table_name || '.' || k.column_name);
END;
END LOOP;
END;
/

Dynamically create tables based on the total counts of another table

I want to be able to create tables based on the total rows in another table.
Let's say I've a table A, and the count is 500K, and so I wanna be able to create 5 tables each of 100K dynamically inside a procedure in oracle.
table A will have counts changing each time, but I would still want to be able to create tables with 100K max, for instance, tomorrow the table A has 550K, I want to be able to create 6 tables with 5 tables having 100k and last one with 50k.
SET SERVEROUTPUT ON
DECLARE
ttl_tables NUMBER;
var_loop NUMBER := 0;
BEGIN
SELECT CAST(COUNT(*)/(500000) AS INT) INTO ttl_tables FROM
px_extract_checks;
DBMS_OUTPUT.PUT_LINE(ttl_tables);
LOOP
var_loop := var_loop + 1;
EXECUTE IMMEDIATE (' || CREATE TABLE ' || 'table' || var_loop || ' ' ||
(Card_Number) || ');
EXIT WHEN var_loop = ttl_tables;
END LOOP;
END;
Something like above.
I have created a small block to help you out using dynamic queries.
You need to use actual column names and little bit changes in the following code as per your requirement:
See inline comments for more information on steps.
BEGIN
-- BEFORE CREATING THE TABLES, NEED TO DROP THE TABLES
FOR T IN (
SELECT
TABLE_NAME
FROM
USER_TABLES
WHERE
TABLE_NAME LIKE 'MY_TABLES_DYNAMIC%'
) LOOP
EXECUTE IMMEDIATE 'DROP TABLE ' || T.TABLE_NAME;
END LOOP;
-- CREATING THE TABLES USING TABLE_A
FOR I IN (
SELECT
CEIL(COUNT(1) / 100000) AS CNT -- DIVIDED THE COUNT OF ROWS BY 100K
FROM
TABLE_A
) LOOP
-- USING CTAS
EXECUTE IMMEDIATE 'CREATE TABLE MY_TABLES_DYNAMIC_'
|| I.CNT
|| ' AS '
|| ' SELECT COL1, COL2, ... FROM'
|| ' (SELECT A.COL1, A.COL2, ...., ROW_NUMBER() OVER (ORDER BY A.PRIMARY_KEY_OF_TABLE_A) RN '
|| ' FROM TABLE_A A)'
|| ' WHERE RN BETWEEN '
|| ( ( I.CNT - 1 ) * 100000 ) + 1
|| ' AND '
|| ( I.CNT * 100000 );
END LOOP;
END;
Cheers!!
Hope this below code will help you for your requirement, you can test it by changing the column names,table names and counts accordingly:
declare
tname_count number:=0;
t_name varchar2(500);
begin
select count(1) into t_count from A;
for k in 1.. t_count
loop
if mod(k,100000)=0 then
tname_count:=tname_count+1;
t_name:='TAB_'||tname_count;
execute immediate 'create table '|| t_name||' (id varchar2(50))';
end if;
end loop;
if floor(t_count/100000)<>0 then
tname_count:=tname_count+1;
t_name:='TAB_'||tname_count;
execute immediate 'create table '|| t_name||' (id varchar2(50))';
end if;
end;

Find all columns that have emails in data oracle

I am going to start off by apologizing because I don't know how to put my problem into words.
i have a database with about 15000 columns and I want to find every column that has emails stored in it. I've tried searching through column names but that isn't helping because there is so much variation.
i want to do something like this
select column_name from all_tab_cols where data like '%#%.com%
this is on an oracle database but I am accessing the data through tableau.
Thanks,
Aayush
Clarification: I want to find every column that has an email address in it.
This will only find email addresses that end with #something.com, but you are looking for something like what is below. There have been other posts about finding email addresses, it is very difficult to do:
DECLARE
l_cmd VARCHAR2 (2000);
l_found INTEGER;
BEGIN
FOR eachcol IN ( SELECT *
FROM all_tab_cols a
WHERE a.data_type = 'VARCHAR2'
AND owner = 'SEARCHSCHEMANAME'
ORDER BY table_name, column_name)
LOOP
l_cmd :=
'select count(*) c from '
|| eachcol.owner
|| '.'
|| eachcol.table_name
|| ' where '
|| LOWER (eachcol.column_name)
|| q'[ LIKE '%#%.com%' AND ROWNUM = 1]';
EXECUTE IMMEDIATE l_cmd INTO l_found;
IF l_found > 0
THEN
DBMS_OUTPUT.put_line (
RPAD (eachcol.owner || '.' || eachcol.table_name || '.' || eachcol.column_name, 92)
|| ' may contain email addresses'
);
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line (l_cmd);
DBMS_OUTPUT.put_line (SQLERRM);
RAISE;
END;

Sort Nested table based on dynamic information

I am having trouble sorted a nested table based on some dynamic information that would be in the order by clause.
Here is a sample of what I have found (https://technology.amis.nl/2006/05/31/sorting-plsql-collections-the-quite-simple-way-part-two-have-the-sql-engine-do-the-heavy-lifting/)
The only difference here is I need to dynamically define the column and direction in the order by clause
SELECT CAST(MULTISET(SELECT *
FROM TABLE(table_a)
ORDER BY P_SORT_COLUMN P_DIRECTION
) as table_typ)
INTO table_b
FROM dual;
So to get around think I thought of using dynamic SQL and put it in a proc as forms cannot do this dynamically
loc_sql_stmt VARCHAR2(500);
BEGIN
loc_sql_stmt := 'SELECT CAST(MULTISET(SELECT * ' ||
'FROM TABLE(P_TABLE_A) ' ||
'ORDER BY P_COLUMN P_DIRECTION || ) as table_typ) ' ||
'INTO P_TABLE_B' ||
'FROM dual;';
EXECUTE IMMEDIATE loc_sql_stmt
USING IN P_TABLE_A, P_COLUMN, P_DIRECTION, P_TABLE_B;
END;
There error I get from the EXECUTE IMMEDIATE line is "ORA-00936 missing expression
So is there a better way to sort a nest table by any given column and the direction or how do I get this dynamic SQL to work?
Here is a sample:
create this in DB:
CREATE OR REPLACE TYPE table_obj AS OBJECT(
column1 VARCHAR2(20),
column2 VARCHAR2(20));
CREATE OR REPLACE TYPE table_typ AS TABLE OF table_obj;
and then a sample run:
DECLARE
table_a table_typ := table_typ ();
table_b table_typ := table_typ ();
loc_idx NUMBER;
loc_sort_column INTEGER := 1;
loc_desc VARCHAR2 (4);
P_SORT_COLUMN VARCHAR2 (100) := 'column1';
P_DIRECTION VARCHAR2 (4) := 'DESC';
loc_sql_stmt VARCHAR2 (500);
BEGIN
FOR i IN 1 .. 5
LOOP
loc_idx := table_a.COUNT + 1;
table_a.EXTEND;
table_a (loc_idx) := table_obj (NULL, NULL);
table_a (loc_idx).column1 := TO_CHAR (loc_idx);
table_a (loc_idx).column2 := TO_CHAR (loc_idx);
END LOOP;
--
loc_sql_stmt :=
'SELECT CAST(MULTISET(SELECT * ' ||
'FROM TABLE(' || table_a || ') ' ||
'ORDER BY ' || P_SORT_COLUMN || ' '|| P_DIRECTION ||
' ) as table_typ) ' ||
'INTO :table_b' ||
'FROM dual';
EXECUTE IMMEDIATE loc_sql_stmt USING IN OUT table_a, table_b;
FOR i IN 1 .. table_b.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE (table_b (i).rx_number);
END LOOP;
END;
To pass variable to native dynamic SQL use : before parameter name, to build dynamic statement use concatenation, like this
loc_sql_stmt VARCHAR2(500);
BEGIN
loc_sql_stmt := 'SELECT CAST(MULTISET(SELECT * ' ||
'FROM TABLE('|| P_TABLE_A || ') ' ||
'ORDER BY ' || P_COLUMN || ', ' || P_DIRECTION || ' ) as table_typ) ' ||
'INTO :P_TABLE_B' ||
'FROM dual;';
EXECUTE IMMEDIATE loc_sql_stmt
USING OUT P_TABLE_B;
END;
EDITED version:
Now seeing your code I understand what you need. To make it work we need to use dynamic PL/SQL block, not Native SQL here is working code of your sample, and pay attention to what is variable and what is concatenated literal
DECLARE
table_a table_typ := table_typ();
table_b table_typ := table_typ();
loc_idx NUMBER;
loc_sort_column INTEGER := 1;
loc_desc VARCHAR2(4);
P_SORT_COLUMN VARCHAR2(100) := 'column1';
P_DIRECTION VARCHAR2(4) := 'desc';
loc_sql_stmt VARCHAR2(500);
BEGIN
FOR i IN 1 .. 5
LOOP
loc_idx := table_a.COUNT + 1;
table_a.EXTEND;
table_a(loc_idx) := table_obj(NULL, NULL);
table_a(loc_idx).column1 := TO_CHAR(loc_idx);
table_a(loc_idx).column2 := TO_CHAR(loc_idx);
END LOOP;
--
loc_sql_stmt := 'begin SELECT CAST(MULTISET(SELECT * ' ||
'FROM TABLE(:table_a ) ORDER BY ' || P_SORT_COLUMN || ' ' ||
P_DIRECTION || ' ) as table_typ ) ' || ' INTO :table_b ' ||
'FROM dual; end;';
EXECUTE IMMEDIATE loc_sql_stmt
USING table_a, IN OUT table_b;
FOR i IN 1 .. table_b.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE(table_b(i).column1);
END LOOP;
END;
If you have limited column/direction choices, try a case statement in your order by, simple example:
select * from tab
order by case when :order = 'c1_asc' then c1 else null end asc
, case when :order = 'c1_desc' then c1 else null end desc
, case when :order = 'c2_asc' then c2 else null end asc
, case when :order = 'c2_desc' then c2 else null end desc
/* ... */
;

Automatically generate sequences and triggers for all tables in Oracle

In my schema, I've migrated about 250 tables from SQL Server to Oracle. The thing is, no sequences or triggers have been created for any of these tables.
Is there an easy way to generate all the table sequences and triggers rather than manually doing this for every table?
An example of a sequence I need would be:
CREATE SEQUENCE "SYSTEM"."SEC_USERS_ID_SEQ"
MINVALUE 0 MAXVALUE 999999999999999999999999
INCREMENT BY 1
START WITH 23
CACHE 20
NOORDER NOCYCLE NOPARTITION;
And the trigger:
create or replace TRIGGER SEC_USERS_TRIG
before INSERT
ON "SYSTEM"."SEC_USERS"
FOR EACH row
BEGIN
IF inserting THEN
IF :NEW."ID" IS NULL THEN
SELECT SEC_USERS_ID_SEQ.nextval INTO :NEW."ID" FROM dual;
END IF;
END IF;
END;
We can generate scripts using the Oracle data dictionary views (the equivalent of MSSQL INFORMATION_SCHEMA). Find out more.
This example generates CREATE SEQUENCE statements. I have followed your example and accepted the default values, which don't need to be coded. The sequence name is derived from table name concatenated with column name and suffixed with "_SEQ". Watch out for Oracle's thirty character limit on object names!
This loop dynamically queries the table to get the current maximum value of the Primary Key column, which is used to derive the STARTS WITH clause.
declare
curr_mx number;
begin
for lrec in ( select ucc.table_name
, ucc.column_name
from user_constraints uc
join user_cons_columns ucc
on ucc.table_name = uc.table_name
and ucc.constraint_name = uc.constraint_name
join user_tab_columns utc
on utc.table_name = ucc.table_name
and utc.column_name = ucc.column_name
where uc.constraint_type = 'P' -- primary key
and utc.data_type = 'NUMBER' -- only numeric columns
)
loop
execute immediate 'select max ('|| lrec.column_name ||') from ' ||lrec.table_name
into curr_mx;
if curr_mx is null then
curr_mx := 0;
end if;
dbms_output.put_line('CREATE SEQUENCE "'|| user || '"."'
|| lrec.table_name ||'_'|| lrec.column_name || '_SEQ" '
||' START WITH ' || to_char( curr_mx + 1 ) ||';'
);
end loop;
end;
/
This code uses DBMS_OUTPUT, so you can spool it to a file for later use. If you're using an IDE like SQL Developer you may need to enable DBMS_OUTPUT. Follow the guidance in this StackOverflow answer.
If you can guarantee that all your tables have a primary key which is a numeric column called ID then you can simplify the select statement. Contrariwise, if some of your primary keys are compound constraints you will need to handle that.
Obviously I plumped for generating sequences because they're simpler. Writing the more complex trigger implementation is left as an exercise for the reader :)
thanks for the script. I altered it a little bit and did the trigger implementation. Feel free to use it.
declare
curr_mx number;
counter number;
seq_name varchar2 (30);
trigger_name varchar2 (30);
begin
for lrec in ( select ucc.table_name
, ucc.column_name
from user_constraints uc
join user_cons_columns ucc
on ucc.table_name = uc.table_name
and ucc.constraint_name = uc.constraint_name
join user_tab_columns utc
on utc.table_name = ucc.table_name
and utc.column_name = ucc.column_name
where uc.constraint_type = 'P' -- primary key
and utc.data_type = 'NUMBER' -- only numeric columns
)
loop
execute immediate 'select (max ('|| lrec.column_name ||')+1) from ' ||lrec.table_name
into curr_mx;
IF curr_mx is null THEN
curr_mx := 0;
END IF;
IF counter is null THEN
counter := 0;
END IF;
/* check length of sequence name, 30 is max */
IF length(lrec.table_name ||'_'|| lrec.column_name || '_SEQ') > 30 THEN
IF length(lrec.column_name || '_SEQ') > 30 THEN
seq_name := counter || '_PKA_SEQ';
ELSE
seq_name := lrec.column_name || '_SEQ';
END IF;
ELSE
seq_name := lrec.table_name ||'_'|| lrec.column_name || '_SEQ';
END IF;
/* check length of trigger name, 30 is max */
IF length(lrec.table_name || '_PKA_T') > 30 THEN
trigger_name := counter || '_PKA_T';
ELSE
trigger_name := lrec.table_name || '_PKA_T';
END IF;
counter := counter +1;
dbms_output.put_line(
'CREATE SEQUENCE "' || seq_name || '"'
||' START WITH ' || to_char( curr_mx + 1 ) ||';'
);
dbms_output.put_line('/');
dbms_output.put_line(
'CREATE OR REPLACE TRIGGER "' || trigger_name || '"'
|| ' BEFORE INSERT ON "' || lrec.table_name || '"'
|| ' FOR EACH ROW '
|| ' BEGIN '
|| ' :new."' || lrec.column_name || '" := "' || seq_name || '".nextval;'
|| ' END;'
);
dbms_output.put_line('/');
end loop;
end;
I also checked if the names of the sequences and triggers are longer than 30 characters because oracle won´t accept these.
EDIT:
Had to put '/' after each line so you can execute all statements at one run.

Resources