0

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');
Ankit Bajpai
  • 13,128
  • 4
  • 25
  • 40
Beefstu
  • 804
  • 6
  • 11
  • @Ankit no it doesn't. My question is why is an INTO needed? The SQL works perfectly when not in the procedure and p_start_date and p_end_date are hard coded dates in the SQL – Beefstu Jul 30 '21 at 18:19
  • 1
    In your procedure create_access_history, You are only selecting the data and in Oracle you cannot return the data only by selecting it. So you have to use some other technique like SYS_REFCURSOR to return it. Since System is expecting that you should store the selected data into a variable thats why it is saying an into clause is mandatory. – Ankit Bajpai Jul 30 '21 at 18:27
  • 2
    From the duplicate answer, "In PLSQL block, columns of select statements must be assigned to variables, which is not the case in SQL statements." That is how oracle works. Not much else to say about it. – OldProgrammer Jul 30 '21 at 19:22
  • 1
    There is a difference between a SQL query (that returns a resultset) and a PL/SQL block/procedure (that does something - in your case with a resultset). In the PL/SQL block you do something with the results of the query - only way to do that is to store the results in variables. Hence INTO is needed. – Koen Lostrie Jul 30 '21 at 19:25
  • I'm inserting the results directly into a table, so why would I need an INTO clause? I only am using PLSQL as a wrapper so I don't have to hard code dates in my SQL. How can I easily get my code to work without rewriting a bunch of SQL. Can you please show me a working example or tell me how to solve my issue. Thanks in advance for your response and help – Beefstu Jul 30 '21 at 20:35
  • @Ankit so I have to rewrite all my working SQL just to pass in two dates? That sounds ridiculous. Can you show me an example of what you are referring too – Beefstu Jul 30 '21 at 20:39
  • 2
    You do not "inserting the results directly into a table", because the only statement in your problematic procedure is `select`. You may consider to spend some time on [2Day Developer Guide](https://docs.oracle.com/en/database/oracle/oracle-database/19/tdddg/stored-subprograms-packages.html#GUID-592F12C2-5BD0-434F-AF77-7CE941223A89) to learn the basics of SQL and PL/SQL in Oracle and get more systematic and structured development flow. – astentx Jul 30 '21 at 21:09
  • @Ankit problem solved. Someone checked in the wrong version of code into source safe. Note there isn't an INSERT statement in the procedure. Apologies for the inconvenience – Beefstu Jul 31 '21 at 19:21

1 Answers1

0

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;
Ankit Bajpai
  • 13,128
  • 4
  • 25
  • 40