Oracle procedure inserting multiple rows from result of query - sql

I am attempting to create a procedure that will INSERT multiple rows into a table from the results of a query in the procedure.
The setup below works fine but I am having difficulty inserting the employee_id, timeoff_date from the output of the query into the timeoff table.
Below is my test CASE. I'm testing in live sql so we can both have the same Oracle version. Any help would be greatly appreciated.
CREATE OR REPLACE TYPE obj_date IS OBJECT (
date_val DATE
);
CREATE OR REPLACE TYPE nt_date IS TABLE OF obj_date;
create or replace function generate_dates_pipelined(
p_from in date,
p_to in date
)
return nt_date
pipelined
is
begin
for c1 in (
with calendar (start_date, end_date ) as (
select trunc(p_from), trunc(p_to) from dual
union all
select start_date + 1, end_date
from calendar
where start_date + 1 <= end_date
)
select start_date as day
from calendar
) loop
pipe row (obj_date(c1.day));
end loop;
return;
end generate_dates_pipelined;
create table holidays(
holiday_date DATE not null,
holiday_name VARCHAR2(20),
constraint holidays_pk primary key (holiday_date),
constraint is_midnight check ( holiday_date = trunc ( holiday_date ) )
);
INSERT into holidays (HOLIDAY_DATE,HOLIDAY_NAME) WITH dts as (
select to_date('01-AUG-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'August 1st 2021' from dual union all
select to_date('05-AUG-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'August 5th 2021' from dual) SELECT * from dts;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees
ADD ( CONSTRAINT employees_pk
PRIMARY KEY (employee_id));
INSERT INTO employees
(
EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days)
WITH names AS (
SELECT 1, 'Jane', 'Doe','F123456', 'NYYYYYN' FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith','R33432','NYYYYYN'FROM dual UNION ALL
SELECT 3, 'Justin', 'Case','C765341','NYYYYYN'FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311','NYYYYYN' FROM dual) SELECT * FROM names;
create table timeoff(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
timeoff_date DATE,
timeoff_type VARCHAR2(1) DEFAULT 'V',
constraint timeoff_chk check (timeoff_date=trunc(timeoff_date, 'dd')),
constraint timeoff_pk primary key (employee_id, timeoff_date)
);
CREATE OR REPLACE PROCEDURE create_timeoff_requests (start_date DATE, end_date DATE)
IS
type t_date is table of date;
l_res t_date;
BEGIN
SELECT
c.date_val
BULK COLLECT INTO l_res
FROM employees e
INNER JOIN TABLE (generate_dates_pipelined (start_date, end_date))c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.date_val) - TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
ORDER BY
e.employee_id,
c.date_val;
-- debug
-- for i in 1..l_res.count -- loop
--dbms_output.put_line(l_res(i));
-- end loop;
END;
EXEC create_timeoff_requests (DATE '2021-08-01', DATE '2021-08-10');

I think it might be easier to create a procedure which looks like this (Just replace the declared constants in the block with parameters in your procedure definition):
CREATE PROCEDURE create_timeoff_requests
(
p_dStart DATE,
p_dEnd DATE,
p_nEmployeeID INTEGER
p_sType VARCHAR2
)
IS
BEGIN
INSERT INTO timeoff (employee_id, timeoff_date, timeoff_type)
SELECT e.employee_id, do.day_off, p_sType
FROM employees e
CROSS JOIN (SELECT p_dStart+LEVEL AS DAY_OFF
FROM DUAL
CONNECT BY LEVEL <= p_dEnd - p_dStart) do
WHERE e.employee_id = p_nEmployeeID
AND SUBSTR(e.workdays, TO_CHAR(do.day_off, 'D'), 1) = 'Y'
AND NOT EXISTS (SELECT 'X' FROM holidays h WHERE h.holiday_date = do.day_off);
END;
/

Related

Oracle converting a working query to a subquery

I have a test case below, which is working fine. In the first scenario I am repeating using TRUC(access_date) so the grouping will work properly.
I want to use a subquery only once to do the TRUNC(access_date) to make the code more legible and only do the TRUNC command in one place but I am having difficulty making it work.
Can someone show me how to resolve this issue. I provided the working version and my attempt, which I couldn't work.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees
ADD (
CONSTRAINT employees_pk PRIMARY KEY (employee_id)
);
INSERT INTO employees (
EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'Jane', 'Doe', 'F123456', 'NYYYYYN' FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith', 'R33432','NYYYYYN' FROM dual UNION ALL
SELECT 3, 'Justin', 'Case', 'C765341','NYYYYYN' FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311','NYYYYYN' FROM dual )
SELECT * FROM names;
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 3;
ALTER TABLE locations
ADD ( CONSTRAINT locations_pk
PRIMARY KEY (location_id));
create table access_history(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
card_num varchar2(10),
location_id number(4),
access_date date,
processed NUMBER(1) default 0
);
create or replace procedure create_access_history(p_start_date date, p_end_date date)
IS
BEGIN
INSERT into access_history
(
employee_id,
card_num,
location_id,
access_date
)
WITH cntr AS
(
SELECT LEVEL - 1 AS n
FROM dual
CONNECT BY LEVEL <= 15 -- Max number of rows per employee per date
)
, got_location_num AS
(
SELECT location_id
, ROW_NUMBER () OVER (ORDER BY location_id) AS location_num
, COUNT (*) OVER () AS max_location_num
FROM locations
)
, employee_days AS
(
SELECT e.employee_id, e.card_num
, d.column_value AS access_date
, dbms_random.value (0, 15) AS rn -- 0 to max number of rows per employee per date
FROM employees e
CROSS JOIN TABLE (generate_dates_pipelined (p_start_date, p_end_date)) d
)
, employee_n_days AS
(
SELECT ed.employee_id, ed.card_num, ed.access_date
, dbms_random.value (0, 1) AS lrn
FROM employee_days ed
JOIN cntr c ON c.n <= ed.rn
)
SELECT n.employee_id, n.card_num, l.location_id,
n.access_date + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND')
FROM employee_n_days n
JOIN got_location_num l ON l.location_num = CEIL (n.lrn * l.max_location_num);
END;
EXEC create_access_history (DATE '2021-08-06', DATE '2021-08-11');
-- Works
select TRUNC(a. access_date) access_date,
e.employee_id,
count(*) cnt
FROM employees e
JOIN access_history a ON a.employee_id = e.employee_id
JOIN locations l ON l.location_id = a.location_id
GROUP BY GROUPING SETS (
(TRUNC(a.access_date), e.employee_id),
(trunc(a.access_date)),
()
)
ORDER BY trunc(a. access_date),e.employee_id;
Fixed
select
access_date,
employee_id,
count(*)
FROM
(
select TRUNC(a.access_date) as access_date,
e.employee_id
FROM employees e
JOIN access_history a ON a.employee_id = e.employee_id
JOIN locations l ON l.location_id = a.location_id ) ah_trunc
GROUP BY GROUPING SETS (
(access_date, employee_id),
(access_date),
()
)
ORDER BY access_date,employee_id;
If I read this correctly, it looks like the issue can be simplified to the following.
Why does this query fail with "invalid identifier"?
select dummy
from ( select lower(dummy) from dual );
ERROR at line 1:
ORA-00904: "DUMMY": invalid identifier
It's because the inline view only has a column named "LOWER(DUMMY)". You either need to refer to that (complete with double quotes), or else give it a name:
select dummy
from ( select lower(dummy) as dummy from dual );
DUMMY
------
x
1 row selected.

Oracle difficulty creating a procedure that has subquery

I am attempting to build a procedure that will INSERT rows into the table emp_attendance.
I call a procedure that generates a list of dates based on a range. I then join that table with each employee_id.
Being a novice SQL developer, I am having difficulty trying to understand why the procedure create_emp_attendance is not being created.
Below is my test CASE. Once I get the rows working for the SELECT I will add the INSERT code as I am trying to take the one little piece at a time.
Thanks in advance for your help and expertise.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
CREATE SEQUENCE batch_seq
START WITH 1
MAXVALUE 999999999999999999999999999
MINVALUE 1
NOCYCLE
CACHE 20
NOORDER;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
INSERT INTO employees (
employee_id,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'John', 'Doe', 'D564311','YYYYYNN' FROM dual UNION ALL
SELECT 2, 'Justin', 'Case', 'C224311','YYYYYNN' FROM dual UNION ALL
SELECT 3, 'Mike', 'Jones', 'J288811','YYYYYNN' FROM dual UNION ALL
SELECT 4, 'Jane', 'Smith', 'S564661','YYYYYNN' FROM dual
) SELECT * FROM names;
CREATE TABLE emp_attendance(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
start_date DATE,
end_date DATE,
week_number NUMBER(2),
create_date DATE DEFAULT SYSDATE
);
CREATE OR REPLACE PROCEDURE create_emp_attendance (
p_start_date IN DATE,
p_end_date IN DATE
)
IS l_batch_seq number;
BEGIN
SELECT get_batch_seq INTO l_batch_seq FROM dual;
SELECT
employee_id,
start_date,
start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date,
to_char(start_date,'WW') AS week_number
FROM (
-- Need subquery to generate end_date based on start_date.
SELECT
e.employee_id, d.COLUMN_VALUE+ NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
FROM employees e
INNER JOIN TABLE( generate_dates_pipelined(p_start_date, p_end_date)
) d
) ed
END;
EXEC create_emp_attendanc(DATE '2021-08-07', DATE '2021-08-14');
You didn't refer to the sequence, so I removed that. Your SELECT needs to have a target via INTO clause or be used within the context of an INSERT (or other) statement. The JOIN was missing an ON clause. But you appeared to want a CROSS JOIN.
If you really want to INSERT in one statement, here's the form:
CREATE OR REPLACE PROCEDURE create_emp_attendance (
p_start_date IN DATE,
p_end_date IN DATE
)
IS
BEGIN
INSERT INTO emp_attendance (employee_id, start_date, end_date, week_number)
SELECT employee_id
, start_date
, start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date
, to_char(start_date,'WW') AS week_number
FROM ( -- Need subquery to generate end_date based on start_date.
SELECT e.employee_id, d.COLUMN_VALUE + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
FROM employees e
CROSS JOIN TABLE( generate_dates_pipelined(p_start_date, p_end_date) ) d
) ed
;
END;
/
EXEC create_emp_attendance(DATE '2021-08-07', DATE '2021-08-14');
/
-- Procedure CREATE_EMP_ATTENDANCE compiled
-- PL/SQL procedure successfully completed.
Read comments within code.
SQL> CREATE OR REPLACE PROCEDURE create_emp_attendance
2 (
3 p_start_date IN DATE,
4 p_end_date IN DATE
5 )
6 IS
7 l_batch_seq number;
8 BEGIN
9 -- there's no GET_BATCH_SEQ function (at least, you didn't post it)
10 -- SELECT get_batch_seq INTO l_batch_seq FROM dual;
11 l_batch_seq := batch_seq.nextval;
12
13 -- In order to avoid TOO_MANY_ROWS, switching to a cursor FOR loop
14 for cur_r in
15 (SELECT
16 employee_id,
17 start_date,
18 start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date,
19 to_char(start_date,'WW') AS week_number
20 FROM (-- Need subquery to generate end_date based on start_date.
21 SELECT
22 e.employee_id,
23 d.COLUMN_VALUE + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
24 FROM employees e
25 -- not INNER, but CROSS join (or, if it were INNER, on which column(s)?)
26 CROSS JOIN TABLE(generate_dates_pipelined(p_start_date, p_end_date)) d
27 ) ed
28 ) loop
29 -- you'll probably have INSERT statement here, according to what you said
30 null;
31 end loop;
32 END;
33 /
Procedure created.
Testing:
SQL> EXEC create_emp_attendance(DATE '2021-08-07', DATE '2021-08-14');
PL/SQL procedure successfully completed.
SQL>

Oracle PLSQL problems creating a procedure

I am trying to wrap some SQL into a PLSQL procedure so a user can pass parameters instead of manually editing a WHERE clause, which would give them the potential to break the working code. The SQL code, which I'm porting is embedded in the PROCEDURE with the exception of the INTO clause.
I know in PLSQL in order to SELECT rows there needs to be an INTO clause. After looking around I saw an example, which creates an object and table type. Something I didn't want to do and seems overly complicated to me. If possible I want to keep everything local to the procedure.
I'm also open to perhaps using BULK collect on the access_history table if that would be a more efficient method.
When I try creating the procedure below It doesn't work and this is where I can use some help and PLSQL expertise to guide me in the best direction to produce the desired data.
Secondly, is there a way to use a DEFAULT value to determine the number of rows to be retrieved.
If the procedure is called like this:
EXEC LAST_EMPLOYEE_HISTORY(1) this means get the last 20 (DEFAULT) rows for employee_id=1, where 20 is the default value.
If the procedure is called like this:
EXEC LAST_EMPLOYEE_HISTORY(1, 50) means get the last 50 rows for employee_id=1.
Any help and expertise in explaininf and helping me fix my issues would be greatly appreciated. Thanks in advance to all who answer.
Below is my test CASE.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE access_history_obj AS OBJECT(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
location_id NUMBER(6),
location_name VARCHAR2(30),
access_date DATE
);
CREATE OR REPLACE TYPE access_history_table IS TABLE OF access_history_obj;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
INSERT INTO employees (
employee_id,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'John', 'Doe', 'D564311','YYYYYNN' FROM dual UNION ALL
SELECT 2, 'Justin', 'Case', 'C224311','YYYYYNN' FROM dual UNION ALL
SELECT 3, 'Mike', 'Jones', 'J288811','YYYYYNN' FROM dual UNION ALL
SELECT 4, 'Jane', 'Smith', 'S564661','YYYYYNN' FROM dual
) SELECT * FROM names;
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 5;
create table access_history(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
card_num varchar2(10),
location_id number(4),
access_date date,
processed NUMBER(1) default 0
);
INSERT INTO access_history(
employee_id,
card_num,
location_id,
access_date
)
WITH rws AS (
SELECT 1,'J11111',2,TO_DATE('2021/08/15 08:30:25', 'YYYY/MM/DD HH24:MI:SS') FROM dual UNION ALL
SELECT 1,'J11111',3,TO_DATE('2021/08/15 18:30:35', 'YYYY/MM/DD HH24:MI:SS') FROM dual UNION ALL
SELECT 2,'E11111',2,TO_DATE('2021/08/15 11:20:35', 'YYYY/MM/DD HH24:MI:SS') FROM dual) SELECT * FROM rws;
CREATE OR REPLACE PROCEDURE LAST_EMPLOYEE_HISTORY(
p_employee_id IN NUMBER,
p_rws IN number)
AS
BEGIN
with rws as (
select e.employee_id,
e.first_name,
e.last_name,
e.card_num,
l.location_id,
l.location_name,
a.access_date,
row_number () over
(
partition by e.employee_id
order by a.access_date DESC
) rn
FROM employees e
JOIN access_history a ON a.employee_id = e.employee_id
JOIN locations l ON l.location_id = a.location_id
)
select employee_id,
first_name,
last_name,
card_num,
location_id,
location_name,
access_date INTO access_history_table
from rws
where
employee_id = p_employee_id AND
rn <= p_rws
order by employee_id, access_date desc;
END;
EXEC LAST_EMPLOYEE_HISTORY(1)
Have a cursor as an OUT parameter and use DEFAULT in the signature of the procedure:
CREATE PROCEDURE LAST_EMPLOYEE_HISTORY(
i_employee_id IN EMPLOYEES.EMPLOYEE_ID%TYPE,
i_rws IN PLS_INTEGER DEFAULT 20,
o_cursor OUT SYS_REFCURSOR
)
AS
BEGIN
OPEN o_cursor FOR
SELECT e.employee_id,
e.first_name,
e.last_name,
e.card_num,
l.location_id,
l.location_name,
a.access_date
FROM employees e
INNER JOIN access_history a
ON a.employee_id = e.employee_id
INNER JOIN locations l
ON l.location_id = a.location_id
WHERE e.employee_id = i_employee_id
ORDER BY access_date DESC
FETCH FIRST i_rws ROWS ONLY;
END;
/
Then in SQL/Plus or SQL Developer:
VARIABLE cur REFCURSOR;
EXECUTE LAST_EMPLOYEE_HISTORY(1, 50, :cur);
PRINT cur;
db<>fiddle here
Note: From Oracle 12, you can the use FETCH FIRST n ROWS ONLY syntax.

PLSQL join results of a pipelined function to a table

I have some SQL(see below), which works fine.
INSERT INTO timeoff (employee_id, timeoff_date)
SELECT
e.employee_id,
c.date_val
FROM employees e
INNER JOIN table(
generate_dates_pipelined(date '2021-08-01', DATE '2021-08-10')
) c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.date_val) - TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
ORDER BY
e.employee_id,
c.date_val;
I am attempting to create a function that will INSERT data as I want to get rid of the hardcoded dates in the SQL.
create table timeoff(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
timeoff_date DATE,
timeoff_type VARCHAR2(1) DEFAULT 'V',
constraint timeoff_chk check (timeoff_date=trunc(timeoff_date, 'dd')),
constraint timeoff_pk primary key (employee_id, timeoff_date)
);
The code in the function works fine in SQL (see above). When I am trying to port it to a procedure I'm getting a syntax error. I believe the problem is with joining the results from my pipelined function, which works fine to the employees table. I'm unsure how to fix this problem.
Below is my test CASE. I'm testing in live sql so we can both have the same Oracle version. As I am a PLSQL novice, could someone please suggest how to fix this issue. Thanks in advance to all that answer.
CREATE OR REPLACE TYPE obj_date IS OBJECT (
date_val DATE
);
CREATE OR REPLACE TYPE nt_date IS TABLE OF obj_date;
create or replace function generate_dates_pipelined(
p_from in date,
p_to in date
)
return nt_date
pipelined
is
begin
for c1 in (
with calendar (start_date, end_date ) as (
select trunc(p_from), trunc(p_to) from dual
union all
select start_date + 1, end_date
from calendar
where start_date + 1 <= end_date
)
select start_date as day
from calendar
) loop
pipe row (obj_date(c1.day));
end loop;
return;
end generate_dates_pipelined;
create table holidays(
holiday_date DATE not null,
holiday_name VARCHAR2(20),
constraint holidays_pk primary key (holiday_date),
constraint is_midnight check ( holiday_date = trunc ( holiday_date ) )
);
INSERT into holidays (HOLIDAY_DATE,HOLIDAY_NAME)
WITH dts as (
select to_date('01-AUG-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'August 1st 2021' from dual union all
select to_date('05-AUG-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'August 5th 2021' from dual
)
SELECT * from dts;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees
ADD (
CONSTRAINT employees_pk PRIMARY KEY (employee_id)
);
INSERT INTO employees (
EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'Jane', 'Doe', 'F123456', 'NYYYYYN' FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith', 'R33432','NYYYYYN' FROM dual UNION ALL
SELECT 3, 'Justin', 'Case', 'C765341','NYYYYYN' FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311','NYYYYYN' FROM dual
)
SELECT * FROM names;
create table timeoff(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
timeoff_date DATE,
timeoff_type VARCHAR2(1) DEFAULT 'V',
constraint timeoff_chk check (timeoff_date=trunc(timeoff_date, 'dd')),
constraint timeoff_pk primary key (employee_id, timeoff_date)
);
-- testing
CREATE OR REPLACE PROCEDURE create_timeoff_requests (
start_date DATE,
end_date DATE
)
IS
type t_date is table of date;
l_res t_date;
BEGIN
SELECT
e.employee_id,
c.date_val
FROM employees e
INNER JOIN ON
BULK COLLECT INTO l_res
FROM TABLE (
generate_dates_pipelined (start_date, end_date)
)c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.date_val) - TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
ORDER BY
e.employee_id,
c.date_val
;
-- debug
for i in 1..l_res.count loop
dbms_output.put_line(l_res(i));
end loop;
END;
EXEC create_timeoff_requests (DATE '2021-08-01', DATE '2021-08-10');
You put BULK COLLECT INTO in a very strange position. I did not check whether your statement logic is correct, but corrected the syntax error.
CREATE OR REPLACE PROCEDURE create_timeoff_requests (start_date DATE, end_date DATE)
IS
type t_date is table of date;
l_res t_date;
BEGIN
SELECT
c.date_val
BULK COLLECT INTO l_res
FROM employees e
INNER JOIN TABLE (generate_dates_pipelined (start_date, end_date))c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.date_val) - TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
ORDER BY
e.employee_id,
c.date_val
;
/**
SELECT e.employee_id,
c.date_val
FROM employees e
INNER JOIN ON
BULK COLLECT INTO l_res
FROM TABLE ( generate_dates_pipelined (start_date, end_date))c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.date_val) - TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
ORDER BY
e.employee_id,
c.date_val
;
**/
-- debug
for i in 1..l_res.count loop
dbms_output.put_line(l_res(i));
end loop;
END;
output
03-AUG-21
04-AUG-21
06-AUG-21
07-AUG-21
10-AUG-21
03-AUG-21
04-AUG-21
06-AUG-21
07-AUG-21
10-AUG-21
03-AUG-21
04-AUG-21
06-AUG-21
07-AUG-21
10-AUG-21
03-AUG-21
04-AUG-21
06-AUG-21
07-AUG-21
10-AUG-21

Oracle error creating procedure expecting iNTO [duplicate]

This question already has an answer here:
PLS-00428: an INTO clause is expected in this SELECT statement
(1 answer)
Closed 1 year ago.
I have some SQL code, which runs perfectly. When I try to wrap it within a procedure it fails to create and I get the following error. Can someone please explain what the issue is and how to fix it?
Below is my test CASE. Thanks in advance to all who answer.
Errors: PROCEDURE CREATE_ACCESS_HISTORY
Line/Col: 4/1 PLS-00428: an INTO clause is expected in this SELECT statement
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
Create table employees(employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees ADD (CONSTRAINT employees_pk PRIMARY KEY (employee_id));
INSERT INTO employees (EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days)
WITH names AS (SELECT 1, 'Jane', 'Doe', 'F123456', 'NYYYYYN'
FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith', 'R33432', 'NYYYYYN'
FROM dual UNION ALL
SELECT 3, 'Justin', 'Case', 'C765341', 'NYYYYYN'
FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311', 'NYYYYYN'
FROM dual )
SELECT * FROM names;
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 50;
ALTER TABLE locations ADD (CONSTRAINT locations_pk PRIMARY KEY (location_id));
create table access_history(seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
card_num varchar2(10),
location_id number(4),
access_date date,
processed NUMBER(1) default 0
);
create or replace procedure create_access_history(p_start_date date, p_end_date date)
IS
BEGIN
WITH cntr AS(SELECT LEVEL - 1 AS n
FROM dual
CONNECT BY LEVEL <= 25 -- Max number of rows per employee per date
)
,got_location_num AS(SELECT location_id,
ROW_NUMBER() OVER (ORDER BY location_id) AS location_num,
COUNT(*) OVER () AS max_location_num
FROM locations)
,employee_days AS(SELECT e.employee_id,
e.card_num,
d.column_value AS access_date,
dbms_random.value (0, 25) AS rn -- 0 to max number of rows per employee per date
FROM employees e
CROSS JOIN TABLE (generate_dates_pipelined (p_start_date, p_end_date)) d)
,employee_n_days AS (SELECT ed.employee_id,
ed.card_num,
ed.access_date,
dbms_random.value (0, 1) AS lrn
FROM employee_days ed
JOIN cntr c ON c.n <= ed.rn
)
SELECT n.employee_id,
n.card_num,
l.location_id,
n.access_date + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS ACCESS_DATE
FROM employee_n_days n
JOIN got_location_num l ON l.location_num = CEIL (n.lrn * l.max_location_num);
END;
EXEC create_access_history (DATE '2021-08-01', DATE '2021-08-10');
Your procedure create_access_history should look alike -
create or replace procedure create_access_history(p_start_date date,
p_end_date date,
result out sys_refcursor)
IS
BEGIN
OPEN result FOR
WITH cntr AS(SELECT LEVEL - 1 AS n
FROM dual
CONNECT BY LEVEL <= 25 -- Max number of rows per employee per date
)
,got_location_num AS(SELECT location_id,
ROW_NUMBER() OVER (ORDER BY location_id) AS location_num,
COUNT(*) OVER () AS max_location_num
FROM locations)
,employee_days AS(SELECT e.employee_id,
e.card_num,
d.column_value AS access_date,
dbms_random.value (0, 25) AS rn -- 0 to max number of rows per employee per date
FROM employees e
CROSS JOIN TABLE (generate_dates_pipelined (p_start_date, p_end_date)) d)
,employee_n_days AS (SELECT ed.employee_id,
ed.card_num,
ed.access_date,
dbms_random.value (0, 1) AS lrn
FROM employee_days ed
JOIN cntr c ON c.n <= ed.rn
)
SELECT n.employee_id,
n.card_num,
l.location_id,
n.access_date + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS ACCESS_DATE
FROM employee_n_days n
JOIN got_location_num l ON l.location_num = CEIL (n.lrn * l.max_location_num);
END;
Then you have to call your procedure like -
DECLARE resultset SYS_REFCURSOR;
BEGIN
EXEC create_access_history (DATE '2021-08-01',
DATE '2021-08-10',
resultset);
FOR I IN 1..resultset.count LOOP
DBMS_OUTPUT.PUT_LINE(I.employee_id || ' ' || I.card_num || ' ' || I.location_id || ' ' || I.ACCESS_DATE);
END LOOP;
END;