Implicit cursor indicates that it updates all rows in the table for a given identifier - sql

I have a simple PL/SQL procedure that increases the salary of an employee in the EMP table of the SCOTT schema. This receives the employee number per parameter and increment. The UPDATE statement that performs the update does not filter by that identifier and when accessing the ROWCOUNT of the cursor indicates that all the rows in the table were updated.
If this update I do from SQL Plus. It only updates a row.
CREATE OR REPLACE PROCEDURE INCREASE_SALARY(
empno EMP.EMPNO%TYPE,
incre NUMBER
)
AUTHID DEFINER
AS PRAGMA AUTONOMOUS_TRANSACTION;
INCREMENT_MUST_BE_GREATER_THAN_ZERO EXCEPTION;
BEGIN
IF incre = 0 THEN
RAISE INCREMENT_MUST_BE_GREATER_THAN_ZERO;
END IF;
DBMS_OUTPUT.PUT_LINE('EMPLOYEE TO UPDATE: ' || empno);
UPDATE EMP
SET SAL = SAL + (SAL * incre / 100)
WHERE EMPNO = empno;
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQL%ROWCOUNT)||' rows affected.');
IF SQL%ROWCOUNT > 0 THEN
INSERT INTO TABLA_LOG VALUES(USER, SYSDATE);
DBMS_OUTPUT.PUT_LINE('SALARY UPDATED SUCCESSFULLY');
ELSE
DBMS_OUTPUT.PUT_LINE('NO EMPLOYEE FOUND FOR THAT ID');
END IF;
COMMIT WORK;
EXCEPTION
WHEN INCREMENT_MUST_BE_GREATER_THAN_ZERO THEN
DBMS_OUTPUT.PUT_LINE('THE INCREMENT PERCENTAGE MUST BE GREATER THAN 0');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('ERROR : ' || SQLCODE || 'MENSAJE: ' || SQLERRM);
END;
/
When executing the procedure with the EMPNO 7900 (JAMES)
SET SERVEROUTPUT ON;
EXEC INCREASE_SALARY(7900, 20);
I get the following output:
EMPLOYEE TO UPDATE: 7900
13 rows affected.
SALARY UPDATED SUCCESSFULLY
Procedimiento PL/SQL terminado correctamente.
Does anyone know I'm doing wrong? Thanks in advance.
select count(1) from EMP WHERE EMPNO = 7900;
Return 1.
I'm using Oracle Enterprise 12c. The procedure compiles. The identifier error too long does not appear.

Your problem is empno = empno. These are interpreted as the column name.
You should try to give your arguments and local variables different names:
CREATE OR REPLACE PROCEDURE INCREASE_SALARY (
in_empno EMP.EMPNO%TYPE,
iin_ncre NUMBER
)
AUTHID DEFINER
AS PRAGMA AUTONOMOUS_TRANSACTION;
INCREMENT_MUST_BE_GREATER_THAN_ZERO EXCEPTION;
BEGIN
IF in_incre = 0 THEN
RAISE INCREMENT_MUST_BE_GREATER_THAN_ZERO;
END IF;
DBMS_OUTPUT.PUT_LINE('EMPLOYEE TO UPDATE: ' || in_empno);
UPDATE EMP
SET SAL = SAL + (SAL * in_incre / 100)
WHERE EMPNO = in_empno;
. . .

Related

Get column names of the result of a query Oracle SQL

I need a query that will get the column names of the result of another query. The other query can be anything - I can't make any assumptions about it but it will typically be some SELECT statement.
For example, if I have this table Members
Id | Name | Age
---|------|----
1 | John | 25
2 | Amir | 13
And this SELECT statement
SELECT Name, Age FROM Members
Then the result of the query I'm trying to write would be
Name
Age
In SQL Server, there is a function - sys.dm_exec_describe_first_result_set - that does this but I can't find an equivalent in Oracle.
I tried to use this answer but I can't use CREATE TYPE statements because of permissions issues and I probably can't use CREATE FUNCTION statements for the same reason.
Suppose you have a query like this:
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
This produces the result:
DEPTNO ANALYST_AVG_SAL CLERK_AVG_SAL SALESMAN_AVG_SAL
---------- --------------- ------------- ----------------
10 1300
20 3000 950
30 950 1400
Notice the column names (like ANALYST_AVG_SAL) - they don't appear exactly in that form anywhere in the query! They are made up from two separate pieces, put together with an underscore.
Now, if you were allowed to create views (note that this does not create any data in your database - it just saves the text of a query), you could do this:
Create the view (just add the first line of code to what we already had):
create view q201028_vw as
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
(Here I assumed you have some way to identify the query, an id like Q201028, and used that in the view name. That is not important, unless you need to do this often and for a large number of queries at the same time.)
Then you can find the column names (and also their order, and - if needed - their data type, etc.) by querying *_TAB_COLUMNS. For example:
select column_id, column_name
from user_tab_columns
where table_name = 'Q201028_VW'
order by column_id
;
COLUMN_ID COLUMN_NAME
---------- --------------------
1 DEPTNO
2 ANALYST_AVG_SAL
3 CLERK_AVG_SAL
4 SALESMAN_AVG_SAL
Now you can drop the view if you don't need it for anything else.
As an aside: The "usual" way to "save" queries in the database, in Oracle, is to create views. If they already exist as such in your DB, then all you need is the last step I showed you. Otherwise, were is the "other query" (for which you need to find the columns) coming from in the first place?
I would use the dbms_sql package and the following code example should show you how to start:
DECLARE
cursorID INTEGER;
status INTEGER;
colCount INTEGER;
rowCount INTEGER;
description dbms_sql.desc_tab;
colType INTEGER;
stringValue VARCHAR2(32676);
sqlCmd VARCHAR2(32767);
BEGIN
-- open cursor
cursorID := dbms_sql.open_cursor;
-- parse statement
dbms_sql.parse(cursorID, 'select * from user_tables', dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- cursor close
dbms_sql.close_cursor(cursorID);
-- open cursor
cursorID := dbms_sql.open_cursor;
-- assemble a new select only using up to 5 the "text" columns
FOR i IN 1 .. description.COUNT LOOP
IF (i > 5) THEN
EXIT;
END IF;
IF (description(i).col_type IN (1, 112)) THEN
IF (sqlCmd IS NOT NULL) THEN
sqlCmd := sqlCmd || ', ';
END IF;
sqlCmd := sqlCmd || description(i).col_name;
END IF;
END LOOP;
sqlCmd := 'SELECT ' || sqlCmd || ' FROM user_tables';
dbms_output.put_line(sqlCmd);
-- parse statement
dbms_sql.parse(cursorID, sqlCmd, dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- define columns
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.define_column(cursorID, i, stringValue, 4000);
END LOOP;
-- execute
status := dbms_sql.execute(cursorID);
-- fetch up to 5 rows
rowCount := 0;
WHILE (dbms_sql.fetch_rows(cursorID) > 0) LOOP
rowCount := rowCount + 1;
IF (rowCount > 5) THEN
EXIT;
END IF;
dbms_output.put_line('row # ' || rowCount);
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.column_value(cursorID, i, stringValue);
dbms_output.put_line('column "' || description(i).col_name || '" = "' || stringValue || '"');
END LOOP;
END LOOP;
-- cursor close
dbms_sql.close_cursor(cursorID);
END;
/
As astentx suggested, you can use a common table expression function to package the PL/SQL code into a SQL statement. This solution is just a single SQL statement, and requires no non-default privileges and does not create any permanent objects.
(The only downside is that not all SQL tools understand these kinds of WITH clauses, and they may throw an error expecting a different statement terminator.)
SQL> create table members(id number, name varchar2(100), age number);
Table created.
SQL> with function get_result_column_names(p_sql varchar2) return sys.odcivarchar2list is
2 v_cursor_id integer;
3 v_col_cnt integer;
4 v_columns dbms_sql.desc_tab;
5 v_column_names sys.odcivarchar2list := sys.odcivarchar2list();
6 begin
7 v_cursor_id := dbms_sql.open_cursor;
8 dbms_sql.parse(v_cursor_id, p_sql, dbms_sql.native);
9 dbms_sql.describe_columns(v_cursor_id, v_col_cnt, v_columns);
10
11 for i in 1 .. v_columns.count loop
12 v_column_names.extend;
13 v_column_names(v_column_names.count) := v_columns(i).col_name;
14 end loop;
15
16 dbms_sql.close_cursor(v_cursor_id);
17
18 return v_column_names;
19 exception when others then
20 dbms_sql.close_cursor(v_cursor_id);
21 raise;
22 end;
23 select *
24 from table(get_result_column_names(q'[select name, age from members]'));
25 /
COLUMN_VALUE
--------------------------------------------------------------------------------
NAME
AGE

How to get rid of compilation error in PL/SQL procedure?

Being new to PL/SQL, I am unable to understand how to manage user-defined exceptions in procedures. My code is giving the Warning 'Procedure created with compilation errors'.
CREATE OR REPLACE PROCEDURE raise_salary
(eid IN employees.e_id%TYPE:=&emp_id,
raise IN employees.salary:=&salary_raise)
IS
cnt INTEGER;
salary employees.salary%TYPE;
BEGIN
SELECT count(*) INTO cnt FROM employees WHERE e_id=eid;
IF cnt=0 THEN
RAISE INVALID_ID;
ELSE
SELECT salary INTO sal FROM employees WHERE e_id=eid;
IF sal IS NULL THEN
RAISE NULL_VALUE;
ELSE
UPDATE employees SET salary=salary+raise WHERE e_id=eid;
dbms_output.put_line('Salary raised!');
END IF;
END IF;
EXCEPTION
WHEN INVALID_ID THEN
dbms_output.put_line('User ID does not exist!');
WHEN NULL_VALUE THEN
dbms_output.put_line('Salary is null in table!');
WHEN others THEN
dbms_output.put_line('Error!');
END;
/
An example:
SQL> CREATE OR REPLACE PROCEDURE testException
2 IS
3 BEGIN
4 raise INVALID_ID;
5 EXCEPTION
6 WHEN INVALID_ID THEN
7 dbms_output.put_line('Invalid ID');
8 END;
9 /
Warning: Procedure created with compilation errors.
A way to know the errors:
SQL> sho err
Errors for PROCEDURE TESTEXCEPTION:
LINE/COL ERROR
-------- -----------------------------------------------------------------
0/0 PL/SQL: Compilation unit analysis terminated
4/5 PL/SQL: Statement ignored
4/11 PLS-00201: identifier 'INVALID_ID' must be declared
6/10 PLS-00201: identifier 'INVALID_ID' must be declared
You need to declare the exceptions you use:
SQL> CREATE OR REPLACE PROCEDURE testException
2 IS
3 INVALID_ID exception;
4 BEGIN
5 raise INVALID_ID;
6 EXCEPTION
7 WHEN INVALID_ID THEN
8 dbms_output.put_line('Invalid ID');
9 END;
10 /
Procedure created.
In this DEMO you will see all 3 situation that this PROCEDURE should handle:
Here is the correct procedure:
CREATE OR REPLACE PROCEDURE raise_salary
(eid IN employees.e_id%TYPE,
raise IN employees.salary%type)
IS
cnt INTEGER;
sal employees.salary%TYPE;
INVALID_ID exception;
NULL_VALUE exception;
BEGIN
SELECT count(*)
INTO cnt
FROM employees
WHERE e_id=eid;
IF cnt=0 THEN
RAISE INVALID_ID;
ELSE
SELECT salary
INTO sal
FROM employees
WHERE e_id=eid;
IF sal IS NULL THEN
RAISE NULL_VALUE;
ELSE
UPDATE employees
SET salary = (SAL + raise)
WHERE e_id = eid;
dbms_output.put_line('Salary raised!');
END IF;
END IF;
exception
WHEN INVALID_ID THEN
dbms_output.put_line('User ID does not exist!');
WHEN NULL_VALUE THEN
dbms_output.put_line('Salary is null in table!');
WHEN others THEN
dbms_output.put_line('Error!');
END;
/
You had more than one errors and one was nicely explained by #Aleksej in his answer VOTE UP.
You also have a line:
SELECT salary INTO sal FROM employees WHERE e_id=eid;
But you have not declared sal.
Hope this helps...

Assigning multiple fields in a loop using execute immediate

I am using PLPDF's libraries to create spreadsheets for various files - I am trying to get a procedure written to take the values of each field and one-by-one insert them into the spreadsheet. This operation can include many different tables going into many different spreadsheets, so just doing an export is not going to cut it.
This example is has two cursors created from tables - the USER_TAB_COLUMNS to select the column names, and the actual view query to pull the data. I have one loop to go through the data record by record, and second to go field-by-field within the record.
Before doing the actual writing to the spreadsheet blob, I'm simply running the dbms_output.putline to make sure that I am getting the data needed.
declare
q_str varchar2(100);
this_vc varchar2(3000);
cursor diet_table is
select * from vcars_diet where nhp_id = 8573;
cursor diet_stru is
select 'begin :1 := i.' || column_name || '; end;' line_o_code from user_tab_columns where table_name = 'VCARS_DIET';
begin
for i in diet_table loop
DBMS_OUTPUT.PUT_LINE ('--------------------------------------------------------');
for h in diet_stru loop
DBMS_OUTPUT.PUT_LINE ('Varchar Value for i: "' || h.line_o_code || '"');
EXECUTE IMMEDIATE (h.line_o_code) USING out this_vc;
DBMS_OUTPUT.PUT_LINE ('Varchar Value for i.' || h.line_o_code || ' is: '||this_vc);
end loop;
end loop;
end;
The fields in the diet table are:
NHP_ID
DATE_TIME
DIET_NO
FORM_NAME
DATA_TYPE
The results are:
ORA-06550: line 1, column 13:
PLS-00201: identifier 'I.NHP_ID' must be declared
ORA-06550: line 1, column 7:
PL/SQL: Statement ignored
ORA-06512: at line 33
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:
I have copied this PL/SQL code from Connor McDonald's solution from this AskTom article.
It uses type dynamic SQL to parse any SQL query and converts the column names as well as values into a collection. I've used a sample query on employees table in HR schema. Replace it with your query.
set serverout on size 999999
set verify off
declare
p_query varchar2(32767) :=
q'{select * from employees
where rownum = 1
}';-- Here you put your query
l_theCursor integer default dbms_sql.open_cursor;
l_columnValue varchar2(4000);
l_status integer;
l_descTbl dbms_sql.desc_tab;
l_colCnt number;
n number := 0;
procedure p(msg varchar2) is
l varchar2(4000) := msg;
begin
while length(l) > 0 loop
dbms_output.put_line(substr(l,1,80));
l := substr(l,81);
end loop;
end;
begin
execute immediate
'alter session set nls_date_format=''dd-mon-yyyy hh24:mi:ss'' ';
dbms_sql.parse( l_theCursor, p_query, dbms_sql.native );
dbms_sql.describe_columns( l_theCursor, l_colCnt, l_descTbl );
for i in 1 .. l_colCnt loop
dbms_sql.define_column(l_theCursor, i, l_columnValue, 4000);
end loop;
l_status := dbms_sql.execute(l_theCursor);
while ( dbms_sql.fetch_rows(l_theCursor) > 0 ) loop
for i in 1 .. l_colCnt loop
dbms_sql.column_value( l_theCursor, i, l_columnValue );
p( 'Value for '|| l_descTbl(i).col_name
|| ' is: ' ||
l_columnValue );
end loop;
dbms_output.put_line( '-----------------' );
n := n + 1;
end loop;
if n = 0 then
dbms_output.put_line( chr(10)||'No data found '||chr(10) );
end if;
end;
/
This gives the output:
Value for EMPLOYEE_ID is: 198
Value for FIRST_NAME is: Donald
Value for LAST_NAME is: OConnell
Value for EMAIL is: DOCONNEL
Value for PHONE_NUMBER is: 650.507.9833
Value for HIRE_DATE is: 21-jun-2007 00:00:00
Value for JOB_ID is: SH_CLERK
Value for SALARY is: 2600
Value for COMMISSION_PCT is:
Value for MANAGER_ID is: 124
Value for DEPARTMENT_ID is: 50
-----------------
PL/SQL procedure successfully completed.

skips a record and uses the previous record data

The question:
Write a PL/SQL block that gives raises to each employee according to the following criteria:
Save as q3.sql no test script required.
• Employees that make less than 1000 get a 6 percent raise
• Employees that make between 1000 and 3000 inclusive get an 8 percent raise
• Employees that make greater than 3000 get a 12 percent raise
With each raise, use dbms_output to print the message ", you just got an n percent raise and your new salary is ". When all raises are completed, rollback the transaction and print the message, "He, he, he...just kidding!"
It does the correct calculations and returns no errors although it's doing the calculations on the previous record and then skipping the next.
SET SERVEROUTPUT ON
DECLARE
CURSOR sal_cursor IS
SELECT ename, sal
FROM EMP
FOR UPDATE OF sal NOWAIT;
V_sal emp.sal%TYPE;
V_ENAME EMP.Ename%TYPE;
BEGIN
FOR emp_record IN sal_cursor
LOOP
FETCH sal_cursor INTO V_Ename, V_Sal;
IF (V_SAL <= 1000) THEN
UPDATE emp
SET EMP.sal = (emp_record.sal * 1.06)
WHERE CURRENT OF sal_cursor;
v_sal := v_sal * 1.06;
ELSIF V_SAL > 1000 AND V_sal <= 3000 THEN
UPDATE emp
SET EMP.sal = (emp_record.sal * 1.08)
WHERE CURRENT OF sal_cursor;
v_sal := v_sal * 1.08;
ELSE
UPDATE emp
SET EMP.sal = (emp_record.sal * 1.12)
WHERE CURRENT OF sal_cursor;
v_sal := v_sal * 1.12;
END IF;
DBMS_OUTPUT.PUT_LINE(V_ENAME || ', you just got an n percent raise
and your new salary is ' || v_sal);
END LOOP;
DBMS_OUTPUT.PUT_LINE('He, he, he... just kidding!');
ROLLBACK;
END;
You are mixing up two ways of handling cursors - explicit and implicit CURSOR FOR loops. You can compare the examples linked from that syntax page.
The FOR emp_record IN sal_cursor method automatically fetches each row into the emp_record variable. Your explicit FETCH then reads the next record. (I'm slightly surprised it doesn't complain about it).
SET SERVEROUTPUT ON
DECLARE
CURSOR sal_cursor IS
SELECT ename, sal
FROM EMP
FOR UPDATE OF sal NOWAIT;
V_sal emp.sal%TYPE;
BEGIN
FOR emp_record IN sal_cursor
LOOP
IF (emp_record.sal <= 1000) THEN
v_sal := emp_record.sal * 1.06;
ELSIF emp_record.sal > 1000 AND emp_record.sal <= 3000 THEN
v_sal := emp_record.sal * 1.08;
ELSE
v_sal := emp_record.sal * 1.12;
END IF;
UPDATE emp
SET EMP.sal = v_sal
WHERE CURRENT OF sal_cursor;
DBMS_OUTPUT.PUT_LINE(emp_record.ename || ', you just got an n percent raise
and your new salary is ' || v_sal);
END LOOP;
DBMS_OUTPUT.PUT_LINE('He, he, he... just kidding!');
ROLLBACK;
END;
You could also use a CASE statement to assign the new value; or use the three separate UPDATE statements and the RETURNING clause to populate v_sal, but don't know if you've learned about those yet.
If you are going to ROLLBACK within the PL/SQL block, you should probably have an exception handler and rollback if an error is encountered as well.
The FOR statement already fetches the record from the cursor. So don't execute FETCH in the following line.

exact fetch returns more than request number of rows when trigger is executed SQL*PLUS

I need to incorporate a Trigger into one of my scripts that uses Cursors. So i have one script with the cursor, and one that creates the trigger (called 'trigEmpRaise'). The trigger is created without errors, however when i run the script with the cursor (which ran fine before the trigger was created) i get these errors:
ERROR at line 1:
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "SCOTT.TRIGEMPRAISE", line 8
ORA-04088: error during execution of trigger 'SCOTT.TRIGEMPRAISE'
ORA-06512: at line 34
Here are the descriptions of the tables:
SQL> describe empcopy
Name Null? Type
----------------------------------------- -------- ----------------------------
EMPNO NUMBER(4)
ENAME VARCHAR2(10)
JOB VARCHAR2(9)
MGR NUMBER(4)
HIREDATE DATE
SAL NUMBER(7,2)
COMM NUMBER(7,2)
DEPTNO NUMBER(2)
SQL> describe emp_prob1;
Name Null? Type
----------------------------------------- -------- ----------------------------
EMPNO NUMBER(4)
ENAME VARCHAR2(10)
DEPTNO NUMBER(2)
SAL NUMBER(7,2)
SQL> describe empraises;
Name Null? Type
----------------------------------------- -------- ----------------------------
EMPNO NUMBER(4)
ENAME VARCHAR2(15)
SAL NUMBER(7,2)
DATEOF DATE
And here are the scripts:
SCRIPT WITH CURSORS (WHICH TRIGGERS THE TRIGGER)
SET SERVEROUTPUT ON
DECLARE
vEMPNO empcopy.empno%TYPE;
VENAME empcopy.ename%TYPE;
vDEPTNO empcopy.deptno%TYPE;
vSAL empcopy.sal%TYPE;
CURSOR deptnoCUR IS
SELECT empno, ename, deptno, sal
FROM empcopy
ORDER BY deptno;
FUNCTION calcSal
(fDEPTNO varchar2, fSAL number)
RETURN NUMBER IS
fNewSal empcopy.sal%TYPE;
BEGIN
IF fDEPTNO = 10 THEN
fNewSal := fSAL + fSAL * .05;
ELSE
IF fDEPTNO = 20 THEN
fNewSal := fSAL + fSAL * .075;
ELSE
fNewSal := fSAL + fSAL * .10;
END IF;
END IF;
RETURN fNewSal;
END calcSal;
BEGIN
OPEN deptnoCUR;
LOOP
FETCH deptnoCUR INTO vEMPNO, vENAME, VDEPTNO, vSAL;
EXIT WHEN deptnoCUR%NOTFOUND;
vSAL := calcSal(vDEPTNO,vSAL);
INSERT INTO emp_prob1
VALUES(vEMPNO,vENAME,vDEPTNO,vSAL);
END LOOP;
CLOSE deptnoCUR;
END;
/
SET SERVEROUTPUT OFF
SELECT * FROM emp_prob1;
AND THE TRIGGER SCRIPT:
CREATE OR REPLACE TRIGGER trigEmpRaise
AFTER INSERT ON emp_prob1
DECLARE
vEMPNO empraises.empno%TYPE;
vENAME empraises.ename%TYPE;
vSAL empraises.sal%TYPE;
vDATE empraises.dateof%TYPE := sysdate;
BEGIN
SELECT empno, ename, sal
INTO vEMPNO, vENAME, vSAL
FROM emp_prob1;
INSERT INTO empraises (empno,ename,sal,dateof)
VALUES(vEMPNO,vENAME,vSAL,vDATE);
END;
/
The script with the cursor simply goes through each record in table EMPCOPY and gives them a raise depending on the DEPTNO they are in. It then inserts the new values into the table EMP_PROB1.
The trigger should occur after it inserts the values into EMP_PROB1, and place the new salaries and the system date the insert occured into table EMPRAISES.
However the error described above keeps occurring no matter what i do. Any help?
SELECT empno, ename, sal
INTO vEMPNO, vENAME, vSAL
FROM emp_prob1; must return exactly 1 row - no more, no less.
My assumuption is you like to do this one:
CREATE OR REPLACE TRIGGER trigEmpRaise
AFTER INSERT ON emp_prob1
FOR EACH ROW
BEGIN
INSERT INTO empraises (empno,ename,sal,dateof)
VALUES(:new.empno,:new.ename,:new.sal,sysdate);
END;
/